create-nextblock 0.2.78 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -1,12 +1,2723 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import { NextRequest, NextResponse } from 'next/server';
4
+ import {
5
+ DeleteObjectsCommand,
6
+ ListObjectsV2Command,
7
+ PutObjectCommand,
8
+ S3Client,
9
+ } from '@aws-sdk/client-s3';
2
10
  import { createClient } from '@supabase/supabase-js';
11
+ import {
12
+ syncFreemiusProductsToSupabase,
13
+ syncSingleFreemiusProduct,
14
+ } from '@nextblock-cms/ecommerce/server';
15
+ import postgres from 'postgres';
16
+
17
+ import { CORTEX_AI_PACKAGE_ID } from '../../../../lib/ai-config';
18
+ import { SANDBOX_RESET_SQL } from './sandboxResetSql';
3
19
 
4
20
  export const dynamic = 'force-dynamic';
21
+ export const maxDuration = 60;
22
+
23
+ type SqlClient = postgres.Sql<Record<string, unknown>>;
24
+ type LanguageId = number | string;
25
+ type SizeSlug = 'small' | 'medium' | 'large';
26
+
27
+ type SeedAsset = {
28
+ source: string;
29
+ dest: string;
30
+ fileName: string;
31
+ contentType: string;
32
+ description?: string;
33
+ };
34
+
35
+ type UploadedSeedAsset = SeedAsset & {
36
+ sizeBytes: number;
37
+ };
38
+
39
+ type MediaStorageRow = {
40
+ id: string;
41
+ object_key: string;
42
+ file_path: string | null;
43
+ };
44
+
45
+ type DescriptionContent = {
46
+ headline: string;
47
+ lead: string;
48
+ whyHeading: string;
49
+ whyParagraph: string;
50
+ bullets: string[];
51
+ };
52
+
53
+ type SeededLocale = {
54
+ title: string;
55
+ slug: string;
56
+ shortDescription: string;
57
+ description: DescriptionContent;
58
+ };
59
+
60
+ type ApparelAccentName = 'amber' | 'sky' | 'rose';
61
+
62
+ type ApparelProductSeed = {
63
+ imageKey: string;
64
+ baseSku: string;
65
+ price: number;
66
+ accent: ApparelAccentName;
67
+ variantStocks: Record<SizeSlug, number>;
68
+ en: SeededLocale;
69
+ fr: SeededLocale;
70
+ };
71
+
72
+ const SANDBOX_COMMERCE_PRODUCT_ID = '24851';
73
+ const SANDBOX_CORTEX_AI_PRODUCT_ID = '28609';
74
+
75
+ const SEEDED_ASSETS: SeedAsset[] = [
76
+ {
77
+ source: 'images/nextblock-logo-small.webp',
78
+ dest: 'images/nextblock-logo-small.webp',
79
+ fileName: 'nextblock-logo-small.webp',
80
+ contentType: 'image/webp',
81
+ description: 'Sandbox seed asset: NextBlock™ logo.',
82
+ },
83
+ {
84
+ source: 'images/goals.webp',
85
+ dest: 'images/goals.webp',
86
+ fileName: 'goals.webp',
87
+ contentType: 'image/webp',
88
+ description: 'Sandbox seed asset: goals illustration.',
89
+ },
90
+ {
91
+ source: 'images/NBcover.webp',
92
+ dest: 'images/NBcover.webp',
93
+ fileName: 'NBcover.webp',
94
+ contentType: 'image/webp',
95
+ description: 'Sandbox seed asset: NextBlock™ architecture cover image.',
96
+ },
97
+ {
98
+ source: 'images/extensibility.webp',
99
+ dest: 'images/extensibility.webp',
100
+ fileName: 'extensibility.webp',
101
+ contentType: 'image/webp',
102
+ description: 'Sandbox seed asset: NextBlock™ extensibility editorial artwork.',
103
+ },
104
+ {
105
+ source: 'images/included.webp',
106
+ dest: 'images/included.webp',
107
+ fileName: 'included.webp',
108
+ contentType: 'image/webp',
109
+ description: 'Sandbox seed asset: NextBlock™ getting-started platform artwork.',
110
+ },
111
+ {
112
+ source: 'images/programmer-upscaled.webp',
113
+ dest: 'images/programmer-upscaled.webp',
114
+ fileName: 'programmer-upscaled.webp',
115
+ contentType: 'image/webp',
116
+ description: 'Sandbox seed asset: programmer hero image.',
117
+ },
118
+ {
119
+ source: 'images/commerce-plan.webp',
120
+ dest: 'images/commerce-plan.webp',
121
+ fileName: 'commerce-plan.webp',
122
+ contentType: 'image/webp',
123
+ description: 'Sandbox seed asset: NextBlock™ commerce roadmap artwork.',
124
+ },
125
+ {
126
+ source: 'images/commerce-square.webp',
127
+ dest: 'images/commerce-square.webp',
128
+ fileName: 'commerce-square.webp',
129
+ contentType: 'image/webp',
130
+ description: 'Sandbox seed asset: Commerce Pro cover image.',
131
+ },
132
+ {
133
+ source: 'images/commerce-wide.webp',
134
+ dest: 'images/commerce-wide.webp',
135
+ fileName: 'commerce-wide.webp',
136
+ contentType: 'image/webp',
137
+ description: 'Sandbox seed asset: NextBlock™ Commerce editorial feature image.',
138
+ },
139
+ {
140
+ source: 'images/cortex-ai.webp',
141
+ dest: 'images/cortex-ai.webp',
142
+ fileName: 'cortex-ai.webp',
143
+ contentType: 'image/webp',
144
+ description: 'Sandbox seed asset: NextBlock Cortex AI editorial feature image.',
145
+ },
146
+ {
147
+ source: 'images/t-shirt.webp',
148
+ dest: 'images/t-shirt.webp',
149
+ fileName: 't-shirt.webp',
150
+ contentType: 'image/webp',
151
+ description: 'Sandbox seed asset: NextBlock™ Studio Tee.',
152
+ },
153
+ {
154
+ source: 'images/cap.webp',
155
+ dest: 'images/cap.webp',
156
+ fileName: 'cap.webp',
157
+ contentType: 'image/webp',
158
+ description: 'Sandbox seed asset: NextBlock™ Signal Cap.',
159
+ },
160
+ {
161
+ source: 'images/pants.webp',
162
+ dest: 'images/pants.webp',
163
+ fileName: 'pants.webp',
164
+ contentType: 'image/webp',
165
+ description: 'Sandbox seed asset: NextBlock™ Utility Pants.',
166
+ },
167
+ {
168
+ source: 'images/cortex-ai-square.webp',
169
+ dest: 'images/cortex-ai-square.webp',
170
+ fileName: 'cortex-ai-square.webp',
171
+ contentType: 'image/webp',
172
+ description: 'Sandbox seed asset: Cortex AI cover image.',
173
+ },
174
+ ];
175
+
176
+ const SIZE_TERM_DEFINITIONS: Array<{
177
+ slug: SizeSlug;
178
+ value: string;
179
+ sortOrder: number;
180
+ frValue: string;
181
+ }> = [
182
+ { slug: 'small', value: 'Small', sortOrder: 0, frValue: 'Petit' },
183
+ { slug: 'medium', value: 'Medium', sortOrder: 1, frValue: 'Moyen' },
184
+ { slug: 'large', value: 'Large', sortOrder: 2, frValue: 'Grand' },
185
+ ];
186
+
187
+ const CORE_MEDIA_RECORDS: Array<{
188
+ assetKey: string;
189
+ description?: string | null;
190
+ }> = [
191
+ {
192
+ assetKey: 'images/nextblock-logo-small.webp',
193
+ description: 'NextBlock™ Site Logo',
194
+ },
195
+ {
196
+ assetKey: 'images/NBcover.webp',
197
+ description: 'NextBlock™ architecture overview cover image',
198
+ },
199
+ {
200
+ assetKey: 'images/extensibility.webp',
201
+ description: 'NextBlock™ extensibility editorial artwork',
202
+ },
203
+ {
204
+ assetKey: 'images/included.webp',
205
+ description: 'NextBlock™ getting-started platform artwork',
206
+ },
207
+ {
208
+ assetKey: 'images/programmer-upscaled.webp',
209
+ description: undefined,
210
+ },
211
+ {
212
+ assetKey: 'images/commerce-plan.webp',
213
+ description: 'NextBlock™ commerce roadmap artwork',
214
+ },
215
+ {
216
+ assetKey: 'images/commerce-wide.webp',
217
+ description: 'NextBlock™ Commerce editorial feature image',
218
+ },
219
+ {
220
+ assetKey: 'images/cortex-ai.webp',
221
+ description: 'NextBlock Cortex AI editorial feature image',
222
+ },
223
+ {
224
+ assetKey: 'images/cortex-ai-square.webp',
225
+ description: 'NextBlock™ Cortex AI cover image',
226
+ },
227
+ ];
228
+
229
+ function getFolderFromObjectKey(objectKey: string) {
230
+ return objectKey.includes('/') ? objectKey.slice(0, objectKey.lastIndexOf('/')) : null;
231
+ }
232
+
233
+
234
+
235
+ type ApparelAccentStyles = {
236
+ heroStops: Array<{ color: string; position: number }>;
237
+ ctaStops: Array<{ color: string; position: number }>;
238
+ eyebrow: string;
239
+ checkBadge: string;
240
+ cardHoverBorder: string;
241
+ ctaSubtext: string;
242
+ };
243
+
244
+ // Per-product accent palettes. The gradient `stops` are inline hex values so they
245
+ // always render; the class strings are written here as literals so Tailwind's
246
+ // content scanner (which includes this file) compiles them into the bundle.
247
+ const APPAREL_ACCENTS: Record<ApparelAccentName, ApparelAccentStyles> = {
248
+ amber: {
249
+ heroStops: [
250
+ { color: '#451a03', position: 0 },
251
+ { color: '#78350f', position: 45 },
252
+ { color: '#0f172a', position: 100 },
253
+ ],
254
+ ctaStops: [
255
+ { color: '#78350f', position: 0 },
256
+ { color: '#451a03', position: 100 },
257
+ ],
258
+ eyebrow: 'text-amber-400',
259
+ checkBadge: 'bg-amber-950 text-amber-300',
260
+ cardHoverBorder: 'hover:border-amber-300 dark:hover:border-amber-500',
261
+ ctaSubtext: 'text-amber-100',
262
+ },
263
+ sky: {
264
+ heroStops: [
265
+ { color: '#082f49', position: 0 },
266
+ { color: '#0c4a6e', position: 40 },
267
+ { color: '#0f172a', position: 100 },
268
+ ],
269
+ ctaStops: [
270
+ { color: '#0c4a6e', position: 0 },
271
+ { color: '#082f49', position: 100 },
272
+ ],
273
+ eyebrow: 'text-sky-400',
274
+ checkBadge: 'bg-sky-950 text-sky-300',
275
+ cardHoverBorder: 'hover:border-sky-300 dark:hover:border-sky-500',
276
+ ctaSubtext: 'text-sky-100',
277
+ },
278
+ rose: {
279
+ heroStops: [
280
+ { color: '#4c0519', position: 0 },
281
+ { color: '#881337', position: 40 },
282
+ { color: '#0f172a', position: 100 },
283
+ ],
284
+ ctaStops: [
285
+ { color: '#881337', position: 0 },
286
+ { color: '#4c0519', position: 100 },
287
+ ],
288
+ eyebrow: 'text-rose-400',
289
+ checkBadge: 'bg-rose-950 text-rose-300',
290
+ cardHoverBorder: 'hover:border-rose-300 dark:hover:border-rose-500',
291
+ ctaSubtext: 'text-rose-100',
292
+ },
293
+ };
294
+
295
+ const APPAREL_COPY: Record<
296
+ 'en' | 'fr',
297
+ { eyebrow: string; ctaHeadline: string; ctaLead: string; ctaButton: string; ctaUrl: string }
298
+ > = {
299
+ en: {
300
+ eyebrow: 'NextBlock™ Apparel',
301
+ ctaHeadline: 'Part of the NextBlock™ demo store',
302
+ ctaLead:
303
+ 'This is a mock product that showcases the commerce engine — multi-currency pricing, size variants, and fully block-based product pages.',
304
+ ctaButton: 'Browse the store',
305
+ ctaUrl: '/shop',
306
+ },
307
+ fr: {
308
+ eyebrow: 'Vêtements NextBlock™',
309
+ ctaHeadline: 'Au cœur de la boutique démo NextBlock™',
310
+ ctaLead:
311
+ "Un article fictif qui illustre le moteur e-commerce — prix multi-devises, variantes de taille et fiches produits entièrement en blocs.",
312
+ ctaButton: 'Voir la boutique',
313
+ ctaUrl: '/boutique',
314
+ },
315
+ };
316
+
317
+ // Build a rich, block-editor-native description (hero + feature cards + CTA) from the
318
+ // structured locale copy, mirroring the digital products so physical seeds are both
319
+ // editable in the block editor and visually on par with the rest of the catalog.
320
+ function buildApparelDescriptionSections(
321
+ content: DescriptionContent,
322
+ accentName: ApparelAccentName,
323
+ localeCode: 'en' | 'fr'
324
+ ) {
325
+ const accent = APPAREL_ACCENTS[accentName];
326
+ const copy = APPAREL_COPY[localeCode];
327
+
328
+ // ── Section 0: Hero (gradient, two columns) ──
329
+ const hero = {
330
+ container_type: 'container',
331
+ background: {
332
+ type: 'gradient',
333
+ gradient: { type: 'linear', direction: '135deg', stops: accent.heroStops },
334
+ },
335
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 2 },
336
+ column_gap: 'xl',
337
+ vertical_alignment: 'center',
338
+ padding: { top: 'xl', bottom: 'xl' },
339
+ column_blocks: [
340
+ [
341
+ {
342
+ block_type: 'text',
343
+ content: {
344
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] ${accent.eyebrow} font-semibold mb-4">${copy.eyebrow}</p>
345
+ <h2 class="text-3xl md:text-5xl font-extrabold text-white leading-tight mb-5">${content.headline}</h2>
346
+ <p class="text-base md:text-lg text-slate-200 leading-relaxed">${content.lead}</p>`,
347
+ },
348
+ },
349
+ ],
350
+ [
351
+ {
352
+ block_type: 'text',
353
+ content: {
354
+ html_content: `<div class="rounded-2xl border border-slate-700 bg-slate-950 p-6 shadow-xl sm:p-8">
355
+ <h3 class="text-lg font-bold text-white mb-3">${content.whyHeading}</h3>
356
+ <p class="text-sm leading-relaxed text-slate-300">${content.whyParagraph}</p>
357
+ </div>`,
358
+ },
359
+ },
360
+ ],
361
+ ],
362
+ };
363
+
364
+ // ── Section 1: Feature cards (one per bullet) ──
365
+ const featureCard = (bullet: string) => ({
366
+ block_type: 'text',
367
+ content: {
368
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors ${accent.cardHoverBorder} hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 sm:p-7">
369
+ <div class="mb-4 inline-flex h-9 w-9 items-center justify-center rounded-full ${accent.checkBadge} text-sm font-bold">✓</div>
370
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">${bullet}</p>
371
+ </div>`,
372
+ },
373
+ });
374
+
375
+ const features = {
376
+ container_type: 'container',
377
+ background: { type: 'none' },
378
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
379
+ column_gap: 'lg',
380
+ padding: { top: 'xl', bottom: 'xl' },
381
+ vertical_alignment: 'stretch',
382
+ column_blocks: content.bullets.map((bullet) => [featureCard(bullet)]),
383
+ };
384
+
385
+ // ── Section 2: CTA (accent gradient) ──
386
+ const cta = {
387
+ container_type: 'container',
388
+ background: {
389
+ type: 'gradient',
390
+ gradient: { type: 'linear', direction: '135deg', stops: accent.ctaStops },
391
+ },
392
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
393
+ column_gap: 'none',
394
+ padding: { top: 'xl', bottom: 'xl' },
395
+ vertical_alignment: 'center',
396
+ column_blocks: [
397
+ [
398
+ {
399
+ block_type: 'heading',
400
+ content: {
401
+ level: 2,
402
+ text_content: copy.ctaHeadline,
403
+ textAlign: 'center',
404
+ textColor: 'background',
405
+ },
406
+ },
407
+ {
408
+ block_type: 'text',
409
+ content: {
410
+ html_content: `<p class="text-center ${accent.ctaSubtext} max-w-xl mx-auto mt-2 mb-6">${copy.ctaLead}</p>`,
411
+ },
412
+ },
413
+ {
414
+ block_type: 'button',
415
+ content: {
416
+ text: copy.ctaButton,
417
+ url: copy.ctaUrl,
418
+ variant: 'secondary',
419
+ size: 'lg',
420
+ position: 'center',
421
+ },
422
+ },
423
+ ],
424
+ ],
425
+ };
426
+
427
+ return [hero, features, cta];
428
+ }
429
+
430
+
431
+ const APPAREL_PRODUCT_SEEDS: ApparelProductSeed[] = [
432
+ {
433
+ imageKey: 'images/t-shirt.webp',
434
+ baseSku: 'NB-STUDIO-TEE',
435
+ price: 3200,
436
+ accent: 'amber',
437
+ variantStocks: { small: 8, medium: 12, large: 6 },
438
+ en: {
439
+ title: 'NextBlock™ Studio Tee (Mock Item)',
440
+ slug: 'nextblock-studio-tee',
441
+ shortDescription:
442
+ 'A heavyweight studio tee built for long build sessions, late launches, and every quiet hour between.',
443
+ description: {
444
+ headline: 'Studio uniform for shipping days',
445
+ lead:
446
+ 'The NextBlock™ Studio Tee is cut from premium heavyweight cotton with a clean silhouette that feels equally at home in a workshop, a coworking space, or a midnight deployment window.',
447
+ whyHeading: 'Why it works',
448
+ whyParagraph:
449
+ 'Soft structure, durable fabric, and a relaxed drape make it the kind of shirt you reach for when the work matters and comfort has to keep up.',
450
+ bullets: [
451
+ 'Heavyweight cotton feel with an easy everyday fit.',
452
+ 'Clean visual profile that pairs well with any setup.',
453
+ 'Built to stay comfortable through long build-and-debug sessions.',
454
+ ],
455
+ },
456
+ },
457
+ fr: {
458
+ title: 'T-shirt Studio NextBlock™ (Article fictif)',
459
+ slug: 'nextblock-studio-tee-fr',
460
+ shortDescription:
461
+ 'Un t-shirt lourd et confortable pense pour les longues sessions de build, les lancements tardifs et les jours ou il faut rester dans le flow.',
462
+ description: {
463
+ headline: 'L uniforme du studio pour les jours de livraison',
464
+ lead:
465
+ 'Le T-shirt Studio NextBlock™ mise sur un coton epais, une ligne propre et une allure simple qui fonctionne autant au bureau qu en session de production tardive.',
466
+ whyHeading: 'Pourquoi ca marche',
467
+ whyParagraph:
468
+ 'Sa matiere robuste et sa coupe detendue offrent un bon equilibre entre maintien, confort et style discret pour les longues journees de travail.',
469
+ bullets: [
470
+ 'Toucher coton epais avec une coupe facile a porter.',
471
+ 'Silhouette nette qui reste propre dans tous les contextes.',
472
+ 'Concu pour garder le confort pendant les longues sessions de build.',
473
+ ],
474
+ },
475
+ },
476
+ },
477
+ {
478
+ imageKey: 'images/cap.webp',
479
+ baseSku: 'NB-SIGNAL-CAP',
480
+ price: 2600,
481
+ accent: 'sky',
482
+ variantStocks: { small: 6, medium: 10, large: 6 },
483
+ en: {
484
+ title: 'NextBlock™ Signal Cap (Mock Item)',
485
+ slug: 'nextblock-signal-cap',
486
+ shortDescription:
487
+ 'A clean everyday cap with subtle techwear energy and just enough structure to finish a sharp off-duty kit.',
488
+ description: {
489
+ headline: 'Low-key signal, strong presence',
490
+ lead:
491
+ 'The NextBlock™ Signal Cap brings a crisp shape and understated studio aesthetic to the kind of everyday accessory that quietly pulls an outfit together.',
492
+ whyHeading: 'Why it works',
493
+ whyParagraph:
494
+ 'It keeps the look restrained, modern, and wearable while still feeling intentional enough to stand out in the details.',
495
+ bullets: [
496
+ 'Structured profile with an easy all-day feel.',
497
+ 'Minimal visual language inspired by modern dev studios.',
498
+ 'Simple finishing piece for travel, work, or weekend runs.',
499
+ ],
500
+ },
501
+ },
502
+ fr: {
503
+ title: 'Casquette Signal NextBlock™ (Article fictif)',
504
+ slug: 'nextblock-signal-cap-fr',
505
+ shortDescription:
506
+ 'Une casquette nette et facile a porter, avec une presence sobre et un esprit techwear leger pour tous les jours.',
507
+ description: {
508
+ headline: 'Un signal discret, une vraie allure',
509
+ lead:
510
+ 'La Casquette Signal NextBlock™ apporte une forme propre et une estetique studio minimaliste a un accessoire du quotidien qui complete la tenue sans effort.',
511
+ whyHeading: 'Pourquoi ca marche',
512
+ whyParagraph:
513
+ 'Elle garde un style moderne, simple et portable tout en donnant assez de caractere pour finir une tenue avec intention.',
514
+ bullets: [
515
+ 'Profil structure avec un confort facile toute la journee.',
516
+ 'Langage visuel minimal inspire des studios de dev modernes.',
517
+ 'Piece simple pour le travail, les deplacements ou le week-end.',
518
+ ],
519
+ },
520
+ },
521
+ },
522
+ {
523
+ imageKey: 'images/pants.webp',
524
+ baseSku: 'NB-UTILITY-PANTS',
525
+ price: 6800,
526
+ accent: 'rose',
527
+ variantStocks: { small: 5, medium: 8, large: 5 },
528
+ en: {
529
+ title: 'NextBlock™ Utility Pants (Mock Item)',
530
+ slug: 'nextblock-utility-pants',
531
+ shortDescription:
532
+ 'Tapered utility pants designed for commute-to-keyboard days, with an easy fit that still feels sharp.',
533
+ description: {
534
+ headline: 'Utility comfort with a refined line',
535
+ lead:
536
+ 'The NextBlock™ Utility Pants balance movement, structure, and a clean tapered cut so you can move from city errands to keyboard time without changing the tone.',
537
+ whyHeading: 'Why it works',
538
+ whyParagraph:
539
+ 'They are practical enough for all-day wear but polished enough to feel intentional, making them an easy anchor piece for a modern work uniform.',
540
+ bullets: [
541
+ 'Tapered silhouette that stays neat without feeling tight.',
542
+ 'Comfort-first construction for long seated sessions.',
543
+ 'Versatile styling that fits both commute and studio rhythms.',
544
+ ],
545
+ },
546
+ },
547
+ fr: {
548
+ title: 'Pantalon utilitaire NextBlock™ (Article fictif)',
549
+ slug: 'nextblock-utility-pants-fr',
550
+ shortDescription:
551
+ 'Un pantalon utilitaire a la coupe fuselee pense pour les trajets, les longues heures au clavier et les journees ou il faut rester mobile.',
552
+ description: {
553
+ headline: 'Le confort utilitaire avec une ligne soignee',
554
+ lead:
555
+ 'Le Pantalon utilitaire NextBlock™ equilibre mobilite, maintien et coupe fuselee pour suivre le rythme entre les deplacements, le studio et les longues sessions de travail.',
556
+ whyHeading: 'Pourquoi ca marche',
557
+ whyParagraph:
558
+ 'Il reste assez pratique pour etre porte toute la journee tout en gardant une allure propre, ce qui en fait une base facile pour une garde-robe de travail moderne.',
559
+ bullets: [
560
+ 'Silhouette fuselee nette sans sensation trop serree.',
561
+ 'Construction orientee confort pour les longues sessions assises.',
562
+ 'Style polyvalent pour le trajet, le studio et le quotidien.',
563
+ ],
564
+ },
565
+ },
566
+ },
567
+ ];
568
+
569
+ async function uploadSeedAssets(params: {
570
+ s3: S3Client;
571
+ bucketName: string;
572
+ siteUrl: string;
573
+ }) {
574
+ const uploadedAssets = new Map<string, UploadedSeedAsset>();
575
+
576
+ for (const asset of SEEDED_ASSETS) {
577
+ let buffer: Buffer | undefined;
578
+ const fetchUrl = `${params.siteUrl}/${asset.source}`;
579
+
580
+ // Optimization: If fetching from localhost, try to read from disk first to avoid ECONNRESET
581
+ if (fetchUrl.includes('localhost') || fetchUrl.includes('127.0.0.1')) {
582
+ try {
583
+ const localPath = path.join(process.cwd(), 'apps/nextblock/public', asset.source);
584
+ if (fs.existsSync(localPath)) {
585
+ console.log(`[Sandbox Reset] Loading local asset: ${localPath}`);
586
+ buffer = fs.readFileSync(localPath);
587
+ }
588
+ } catch (err) {
589
+ console.warn(`[Sandbox Reset] Failed to read local asset: ${asset.source}`, err);
590
+ }
591
+ }
592
+
593
+ if (!buffer) {
594
+ console.log(`[Sandbox Reset] Fetching ${fetchUrl}...`);
595
+
596
+ let lastErr: any;
597
+ for (let attempt = 1; attempt <= 3; attempt++) {
598
+ try {
599
+ const res = await fetch(fetchUrl);
600
+ if (!res.ok) {
601
+ throw new Error(`Failed to fetch asset: ${fetchUrl} (${res.status})`);
602
+ }
603
+ buffer = Buffer.from(await res.arrayBuffer());
604
+ break;
605
+ } catch (err) {
606
+ lastErr = err;
607
+ if (attempt === 3) break;
608
+ console.warn(`[Sandbox Reset] Fetch failed (attempt ${attempt}): ${fetchUrl}. Retrying in 1s...`);
609
+ await new Promise((resolve) => setTimeout(resolve, 1000));
610
+ }
611
+ }
612
+
613
+ if (!buffer) {
614
+ throw new Error(`Failed to fetch asset after 3 attempts: ${fetchUrl}. Last error: ${lastErr?.message}`);
615
+ }
616
+ }
617
+
618
+ await params.s3.send(
619
+ new PutObjectCommand({
620
+ Bucket: params.bucketName,
621
+ Key: asset.dest,
622
+ Body: buffer,
623
+ ContentType: asset.contentType,
624
+ })
625
+ );
626
+
627
+ uploadedAssets.set(asset.dest, {
628
+ ...asset,
629
+ sizeBytes: buffer.byteLength,
630
+ });
631
+
632
+ console.log(`[Sandbox Reset] Uploaded ${asset.dest}`);
633
+ }
634
+
635
+ return uploadedAssets;
636
+ }
637
+
638
+ async function upsertMediaRecord(
639
+ sql: SqlClient,
640
+ asset: UploadedSeedAsset,
641
+ description?: string | null
642
+ ) {
643
+ const folder = getFolderFromObjectKey(asset.dest);
644
+ const recordDescription = description === undefined ? asset.description ?? null : description;
645
+ const [mediaRecord] = await sql`
646
+ INSERT INTO public.media (
647
+ file_name,
648
+ object_key,
649
+ file_path,
650
+ file_type,
651
+ size_bytes,
652
+ folder,
653
+ description
654
+ )
655
+ VALUES (
656
+ ${asset.fileName},
657
+ ${asset.dest},
658
+ ${asset.dest},
659
+ ${asset.contentType},
660
+ ${asset.sizeBytes},
661
+ ${folder},
662
+ ${recordDescription}
663
+ )
664
+ ON CONFLICT (object_key) DO UPDATE
665
+ SET
666
+ file_name = EXCLUDED.file_name,
667
+ file_path = EXCLUDED.file_path,
668
+ file_type = EXCLUDED.file_type,
669
+ size_bytes = EXCLUDED.size_bytes,
670
+ folder = EXCLUDED.folder,
671
+ description = EXCLUDED.description,
672
+ updated_at = now()
673
+ RETURNING id
674
+ `;
675
+
676
+ if (!mediaRecord?.id) {
677
+ throw new Error(`Failed to upsert media record for ${asset.dest}`);
678
+ }
679
+
680
+ return mediaRecord.id as string;
681
+ }
682
+
683
+ async function normalizeMediaStorageKeys(sql: SqlClient) {
684
+ const rows = (await sql`
685
+ SELECT id, object_key, file_path
686
+ FROM public.media
687
+ WHERE object_key LIKE '/%' OR file_path LIKE '/%'
688
+ `) as MediaStorageRow[];
689
+
690
+ for (const row of rows) {
691
+ const normalizedObjectKey = row.object_key.replace(/^\/+/, '');
692
+ const normalizedFilePath = (row.file_path || row.object_key).replace(/^\/+/, '');
693
+ const folder = getFolderFromObjectKey(normalizedFilePath);
694
+
695
+ await sql`
696
+ UPDATE public.media
697
+ SET
698
+ object_key = ${normalizedObjectKey},
699
+ file_path = ${normalizedFilePath},
700
+ folder = ${folder},
701
+ updated_at = now()
702
+ WHERE id = ${row.id}
703
+ `;
704
+ }
705
+
706
+ return rows.length;
707
+ }
708
+
709
+ async function ensureCoreMediaRecords(params: {
710
+ sql: SqlClient;
711
+ uploadedAssets: Map<string, UploadedSeedAsset>;
712
+ }) {
713
+ for (const record of CORE_MEDIA_RECORDS) {
714
+ const asset = params.uploadedAssets.get(record.assetKey);
715
+ if (!asset) {
716
+ throw new Error(`Missing uploaded asset for ${record.assetKey}.`);
717
+ }
718
+
719
+ await upsertMediaRecord(params.sql, asset, record.description);
720
+ }
721
+ }
722
+
723
+ async function attachProductMedia(sql: SqlClient, productId: string, mediaId: string) {
724
+ await sql`DELETE FROM public.product_media WHERE product_id = ${productId}`;
725
+ await sql`
726
+ INSERT INTO public.product_media (product_id, media_id, sort_order)
727
+ VALUES (${productId}, ${mediaId}, 0)
728
+ `;
729
+ }
730
+
731
+ async function upsertInventoryItems(
732
+ sql: SqlClient,
733
+ inventoryRows: Array<{ sku: string; quantity: number }>
734
+ ) {
735
+ for (const row of inventoryRows) {
736
+ await sql`
737
+ INSERT INTO public.inventory_items (sku, quantity)
738
+ VALUES (${row.sku}, ${row.quantity})
739
+ ON CONFLICT (sku) DO UPDATE
740
+ SET
741
+ quantity = EXCLUDED.quantity,
742
+ updated_at = now()
743
+ `;
744
+ }
745
+ }
746
+
747
+ async function getLanguageIds(sql: SqlClient) {
748
+ const [enLangRaw] = await sql`SELECT id FROM public.languages WHERE code = 'en' LIMIT 1`;
749
+ const [frLangRaw] = await sql`SELECT id FROM public.languages WHERE code = 'fr' LIMIT 1`;
750
+
751
+ if (!enLangRaw?.id || !frLangRaw?.id) {
752
+ throw new Error('Required languages (en, fr) not found during sandbox enrichment.');
753
+ }
754
+
755
+ return {
756
+ enLangId: enLangRaw.id as LanguageId,
757
+ frLangId: frLangRaw.id as LanguageId,
758
+ };
759
+ }
760
+
761
+ async function ensureSizeAttribute(sql: SqlClient) {
762
+ const [attribute] = await sql`
763
+ INSERT INTO public.product_attributes (name, slug, name_translations)
764
+ VALUES ('Size', 'size', ${sql.json({ fr: 'Taille' })})
765
+ ON CONFLICT (slug) DO UPDATE
766
+ SET
767
+ name = EXCLUDED.name,
768
+ name_translations = EXCLUDED.name_translations,
769
+ updated_at = now()
770
+ RETURNING id
771
+ `;
772
+
773
+ if (!attribute?.id) {
774
+ throw new Error('Failed to seed the Size product attribute.');
775
+ }
776
+
777
+ const termIds = {} as Record<SizeSlug, string>;
778
+
779
+ for (const termDefinition of SIZE_TERM_DEFINITIONS) {
780
+ const [term] = await sql`
781
+ INSERT INTO public.product_attribute_terms (
782
+ attribute_id,
783
+ value,
784
+ slug,
785
+ sort_order,
786
+ value_translations
787
+ )
788
+ VALUES (
789
+ ${attribute.id},
790
+ ${termDefinition.value},
791
+ ${termDefinition.slug},
792
+ ${termDefinition.sortOrder},
793
+ ${sql.json({ fr: termDefinition.frValue })}
794
+ )
795
+ ON CONFLICT ON CONSTRAINT product_attribute_terms_attribute_id_slug_key DO UPDATE
796
+ SET
797
+ value = EXCLUDED.value,
798
+ sort_order = EXCLUDED.sort_order,
799
+ value_translations = EXCLUDED.value_translations,
800
+ updated_at = now()
801
+ RETURNING id
802
+ `;
803
+
804
+ if (!term?.id) {
805
+ throw new Error(`Failed to seed the ${termDefinition.slug} product attribute term.`);
806
+ }
807
+
808
+ termIds[termDefinition.slug] = term.id as string;
809
+ }
810
+
811
+ return termIds;
812
+ }
813
+
814
+ async function enrichCommerceProducts(params: {
815
+ sql: SqlClient;
816
+ commerceAsset: UploadedSeedAsset;
817
+ enLangId: LanguageId;
818
+ frLangId: LanguageId;
819
+ }) {
820
+ console.log('[Sandbox Reset] Enriching NextBlock™ Commerce Pro...');
821
+
822
+ const commerceMediaId = await upsertMediaRecord(
823
+ params.sql,
824
+ params.commerceAsset,
825
+ 'Sandbox seed asset: NextBlock™ Commerce Pro.'
826
+ );
827
+
828
+ const [product] = await params.sql`
829
+ SELECT *
830
+ FROM public.products
831
+ WHERE freemius_product_id = ${SANDBOX_COMMERCE_PRODUCT_ID} AND language_id = ${params.enLangId}
832
+ LIMIT 1
833
+ `;
834
+
835
+ if (!product) {
836
+ throw new Error(
837
+ `Commerce Pro product ${SANDBOX_COMMERCE_PRODUCT_ID} was not found after Freemius sync.`
838
+ );
839
+ }
840
+
841
+ const shortDescEn =
842
+ 'NextBlock™ Commerce Pro is the ultimate AI-native, block-based headless e-commerce engine for Next.js. Deploy fast global storefronts with native multi-currency pricing, Stripe/Freemius checkouts, automatic tax calculations, and flexible shipping zones.';
843
+
844
+ // ── Section 0: Hero Gradient ──
845
+ const commerceS0En = {
846
+ container_type: 'container',
847
+ background: {
848
+ type: 'gradient',
849
+ gradient: {
850
+ type: 'linear',
851
+ direction: '135deg',
852
+ stops: [
853
+ { color: '#022c22', position: 0 },
854
+ { color: '#064e3b', position: 40 },
855
+ { color: '#0f172a', position: 100 },
856
+ ],
857
+ },
858
+ },
859
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 2 },
860
+ column_gap: 'xl',
861
+ vertical_alignment: 'center',
862
+ padding: { top: 'xl', bottom: 'xl' },
863
+ column_blocks: [
864
+ [
865
+ {
866
+ block_type: 'text',
867
+ content: {
868
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4">Enterprise E-Commerce Engine</p>
869
+ <h2 class="text-3xl md:text-5xl font-extrabold text-white leading-tight mb-5">Turn Next.js Into a<br/>Global Storefront.</h2>
870
+ <p class="text-base md:text-lg text-slate-200 leading-relaxed mb-6">Commerce Pro is a composable, developer-first engine that powers physical product catalogs, digital licensing, and subscription commerce — all from your existing Next.js stack.</p>`,
871
+ },
872
+ },
873
+ {
874
+ block_type: 'button',
875
+ content: {
876
+ text: 'Get Commerce Pro →',
877
+ url: 'https://nextblock.dev/product/nextblock-commerce-pro-commerce-license',
878
+ variant: 'default',
879
+ size: 'lg',
880
+ position: 'left',
881
+ },
882
+ },
883
+ ],
884
+ [
885
+ {
886
+ block_type: 'text',
887
+ content: {
888
+ html_content: `<div class="rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8">
889
+ <h3 class="text-lg font-bold text-white mb-5">Why Teams Choose Commerce Pro</h3>
890
+ <ul class="space-y-4 text-sm leading-relaxed text-slate-300">
891
+ <li class="flex items-start gap-3">
892
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
893
+ <span><strong class="text-white">Multi-Currency Pricing</strong> — real-time exchange rates, charm pricing rules, and automatic locale detection.</span>
894
+ </li>
895
+ <li class="flex items-start gap-3">
896
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
897
+ <span><strong class="text-white">Tax Automation</strong> — built-in Stripe Tax integration calculates, collects, and reports in 40+ countries.</span>
898
+ </li>
899
+ <li class="flex items-start gap-3">
900
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
901
+ <span><strong class="text-white">Resilient Stock Tracking</strong> — inventory locks at checkout to prevent over-selling, with per-variant control.</span>
902
+ </li>
903
+ <li class="flex items-start gap-3">
904
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
905
+ <span><strong class="text-white">Flexible Shipping Zones</strong> — rate tables, free-shipping thresholds, and per-country rules out of the box.</span>
906
+ </li>
907
+ </ul>
908
+ </div>`,
909
+ },
910
+ },
911
+ ],
912
+ ],
913
+ };
914
+
915
+ // ── Section 1: Social-proof metrics bar ──
916
+ const commerceS1En = {
917
+ container_type: 'container',
918
+ background: { type: 'theme', theme: 'muted' },
919
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 4 },
920
+ column_gap: 'lg',
921
+ padding: { top: 'lg', bottom: 'lg' },
922
+ vertical_alignment: 'center',
923
+ column_blocks: [
924
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">40+</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Tax Jurisdictions</span></p>' } }],
925
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">135+</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Currencies Supported</span></p>' } }],
926
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">&lt; 50ms</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Cart API Response</span></p>' } }],
927
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">100 %</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Lighthouse Score</span></p>' } }],
928
+ ],
929
+ };
930
+
931
+ // ── Section 2: Three-column feature cards ──
932
+ const commerceS2En = {
933
+ container_type: 'container',
934
+ background: { type: 'none' },
935
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
936
+ column_gap: 'lg',
937
+ padding: { top: 'xl', bottom: 'xl' },
938
+ vertical_alignment: 'stretch',
939
+ column_blocks: [
940
+ [
941
+ {
942
+ block_type: 'text',
943
+ content: {
944
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-emerald-500 sm:p-7">
945
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"><svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="M3 10h18M7 15h3"></path></svg></div>
946
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Headless Stripe Checkout</h4>
947
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Integrated checkout session creation for single or multiple items. Auto-fulfillment fires via webhook events, with built-in idempotency guards.</p>
948
+ </div>`,
949
+ },
950
+ },
951
+ ],
952
+ [
953
+ {
954
+ block_type: 'text',
955
+ content: {
956
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-emerald-500 sm:p-7">
957
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"><svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 7a4 4 0 1 1-2.83 6.83L7 19H4v-3h3l2.17-2.17A4 4 0 0 1 15 7Z"></path><path d="M17.5 8.5h.01"></path></svg></div>
958
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Freemius Digital Licensing</h4>
959
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Distribute software downloads, validate license keys, and manage SaaS trial periods natively — no third-party integration layer required.</p>
960
+ </div>`,
961
+ },
962
+ },
963
+ ],
964
+ [
965
+ {
966
+ block_type: 'text',
967
+ content: {
968
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-emerald-500 sm:p-7">
969
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"><svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"></rect><path d="m4 15 4-4 4 4 3-3 5 5"></path><circle cx="15" cy="9" r="1"></circle></svg></div>
970
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Visual Merchandising</h4>
971
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Content creators can add product details pages, highlight promotions, and build full landing pages — all inside the block editor, no code needed.</p>
972
+ </div>`,
973
+ },
974
+ },
975
+ ],
976
+ ],
977
+ };
978
+
979
+ // ── Section 3: Deep-dive 2-col ──
980
+ const commerceS3En = {
981
+ container_type: 'container',
982
+ background: {
983
+ type: 'gradient',
984
+ gradient: {
985
+ type: 'linear',
986
+ direction: '180deg',
987
+ stops: [
988
+ { color: '#020617', position: 0 },
989
+ { color: '#0f172a', position: 100 },
990
+ ],
991
+ },
992
+ },
993
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 2 },
994
+ column_gap: 'xl',
995
+ vertical_alignment: 'center',
996
+ padding: { top: 'xl', bottom: 'xl' },
997
+ column_blocks: [
998
+ [
999
+ {
1000
+ block_type: 'text',
1001
+ content: {
1002
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4">Under The Hood</p>
1003
+ <h3 class="text-2xl md:text-3xl font-extrabold text-white mb-4">Built for Production Scale</h3>
1004
+ <p class="text-slate-300 leading-relaxed mb-5">Commerce Pro was designed from day one for high-traffic stores. Every API path is edge-cached, every database query is indexed, and every webhook handler is idempotent.</p>
1005
+ <ul class="space-y-3 text-sm text-slate-400">
1006
+ <li class="flex items-start gap-2.5">
1007
+ <span class="text-emerald-400">→</span>
1008
+ <span>Variant-level inventory with optimistic locking</span>
1009
+ </li>
1010
+ <li class="flex items-start gap-2.5">
1011
+ <span class="text-emerald-400">→</span>
1012
+ <span>Automatic shipping zone calculation and rate tables</span>
1013
+ </li>
1014
+ <li class="flex items-start gap-2.5">
1015
+ <span class="text-emerald-400">→</span>
1016
+ <span>Real-time order status with Stripe webhook sync</span>
1017
+ </li>
1018
+ <li class="flex items-start gap-2.5">
1019
+ <span class="text-emerald-400">→</span>
1020
+ <span>Product attributes and filterable facets system</span>
1021
+ </li>
1022
+ </ul>`,
1023
+ },
1024
+ },
1025
+ ],
1026
+ [
1027
+ {
1028
+ block_type: 'text',
1029
+ content: {
1030
+ html_content: `<div class="space-y-4">
1031
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1032
+ <h5 class="text-sm font-bold text-white mb-1">Order Management</h5>
1033
+ <p class="text-xs text-slate-400 leading-relaxed">Full order lifecycle from cart to fulfillment. Automatic status transitions, email notifications, and refund handling baked in.</p>
1034
+ </div>
1035
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1036
+ <h5 class="text-sm font-bold text-white mb-1">Product Categories</h5>
1037
+ <p class="text-xs text-slate-400 leading-relaxed">Hierarchical taxonomy with slugs, media attachments, and full i18n support. Categories are managed directly in the CMS dashboard.</p>
1038
+ </div>
1039
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1040
+ <h5 class="text-sm font-bold text-white mb-1">Multi-Language Storefronts</h5>
1041
+ <p class="text-xs text-slate-400 leading-relaxed">Products, categories, and checkout flows are fully translatable. Each language variant shares inventory and pricing while maintaining its own SEO metadata.</p>
1042
+ </div>
1043
+ </div>`,
1044
+ },
1045
+ },
1046
+ ],
1047
+ ],
1048
+ };
1049
+
1050
+ // ── Section 4: CTA ──
1051
+ const commerceS4En = {
1052
+ container_type: 'container',
1053
+ background: {
1054
+ type: 'gradient',
1055
+ gradient: {
1056
+ type: 'linear',
1057
+ direction: '135deg',
1058
+ stops: [
1059
+ { color: '#064e3b', position: 0 },
1060
+ { color: '#022c22', position: 100 },
1061
+ ],
1062
+ },
1063
+ },
1064
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
1065
+ column_gap: 'none',
1066
+ padding: { top: 'xl', bottom: 'xl' },
1067
+ vertical_alignment: 'center',
1068
+ column_blocks: [
1069
+ [
1070
+ {
1071
+ block_type: 'heading',
1072
+ content: { level: 2, text_content: 'Ready to launch your storefront?', textAlign: 'center', textColor: 'background' },
1073
+ },
1074
+ {
1075
+ block_type: 'text',
1076
+ content: {
1077
+ html_content: '<p class="text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6">Start selling today with Commerce Pro. Multi-currency, tax-compliant, and lightning-fast out of the box.</p>',
1078
+ },
1079
+ },
1080
+ {
1081
+ block_type: 'button',
1082
+ content: {
1083
+ text: 'Purchase Commerce Pro',
1084
+ url: 'https://nextblock.dev/product/nextblock-commerce-pro-commerce-license',
1085
+ variant: 'secondary',
1086
+ size: 'lg',
1087
+ position: 'center',
1088
+ },
1089
+ },
1090
+ ],
1091
+ ],
1092
+ };
1093
+
1094
+ const commerceSectionsEn = [commerceS0En, commerceS1En, commerceS2En, commerceS3En, commerceS4En];
1095
+
1096
+ await params.sql`
1097
+ UPDATE public.products
1098
+ SET
1099
+ title = 'NextBlock™ Commerce Pro - Commerce License',
1100
+ short_description = ${shortDescEn},
1101
+ description_json = NULL,
1102
+ product_type = 'digital',
1103
+ payment_provider = 'freemius'
1104
+ WHERE id = ${product.id}
1105
+ `;
1106
+
1107
+ await attachProductMedia(params.sql, product.id as string, commerceMediaId);
1108
+
1109
+ // Set description blocks for English product
1110
+ await params.sql`DELETE FROM public.blocks WHERE product_id = ${product.id}`;
1111
+ for (let i = 0; i < commerceSectionsEn.length; i++) {
1112
+ await params.sql`
1113
+ INSERT INTO public.blocks (product_id, language_id, block_type, content, "order")
1114
+ VALUES (${product.id}, ${params.enLangId}, 'section', ${params.sql.json(commerceSectionsEn[i] as any)}, ${i})
1115
+ `;
1116
+ }
1117
+
1118
+ // ── French Sections ──
1119
+ const shortDescFr =
1120
+ "NextBlock™ Commerce Pro est le moteur e-commerce headless et orienté IA ultime pour Next.js. Déployez des boutiques mondiales ultra-rapides avec support multi-devises, Stripe/Freemius, taxes automatisées et zones d'expédition.";
1121
+
1122
+ const commerceS0Fr = {
1123
+ ...commerceS0En,
1124
+ column_blocks: [
1125
+ [
1126
+ {
1127
+ block_type: 'text',
1128
+ content: {
1129
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4">Moteur E-Commerce d'Entreprise</p>
1130
+ <h2 class="text-3xl md:text-5xl font-extrabold text-white leading-tight mb-5">Faites de Next.js une<br/>Boutique Mondiale.</h2>
1131
+ <p class="text-base md:text-lg text-slate-200 leading-relaxed mb-6">Commerce Pro est un moteur composable pensé pour les développeurs : catalogues physiques, licences numériques et abonnements — le tout depuis votre stack Next.js existant.</p>`,
1132
+ },
1133
+ },
1134
+ {
1135
+ block_type: 'button',
1136
+ content: {
1137
+ text: 'Obtenir Commerce Pro →',
1138
+ url: 'https://nextblock.dev/product/nextblock-commerce-pro-commerce-license',
1139
+ variant: 'default',
1140
+ size: 'lg',
1141
+ position: 'left',
1142
+ },
1143
+ },
1144
+ ],
1145
+ [
1146
+ {
1147
+ block_type: 'text',
1148
+ content: {
1149
+ html_content: `<div class="rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8">
1150
+ <h3 class="text-lg font-bold text-white mb-5">Pourquoi choisir Commerce Pro</h3>
1151
+ <ul class="space-y-4 text-sm leading-relaxed text-slate-300">
1152
+ <li class="flex items-start gap-3">
1153
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
1154
+ <span><strong class="text-white">Multi-Devises</strong> — taux de change en temps réel, arrondis personnalisés et détection automatique de la localisation.</span>
1155
+ </li>
1156
+ <li class="flex items-start gap-3">
1157
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
1158
+ <span><strong class="text-white">Taxes automatisées</strong> — intégration Stripe Tax pour le calcul, la collecte et le reporting dans plus de 40 pays.</span>
1159
+ </li>
1160
+ <li class="flex items-start gap-3">
1161
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
1162
+ <span><strong class="text-white">Gestion des Stocks</strong> — verrouillage de l'inventaire au checkout pour éviter les surventes, avec contrôle par variante.</span>
1163
+ </li>
1164
+ <li class="flex items-start gap-3">
1165
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-emerald-950 flex items-center justify-center text-emerald-300 text-xs font-bold">✓</span>
1166
+ <span><strong class="text-white">Zones d'expédition flexibles</strong> — tables de tarifs, seuils de livraison gratuite et règles par pays inclus.</span>
1167
+ </li>
1168
+ </ul>
1169
+ </div>`,
1170
+ },
1171
+ },
1172
+ ],
1173
+ ],
1174
+ };
1175
+
1176
+ const commerceS1Fr = {
1177
+ ...commerceS1En,
1178
+ column_blocks: [
1179
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">40+</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Juridictions fiscales</span></p>' } }],
1180
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">135+</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Devises supportées</span></p>' } }],
1181
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">&lt; 50ms</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Réponse API panier</span></p>' } }],
1182
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">100 %</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Score Lighthouse</span></p>' } }],
1183
+ ],
1184
+ };
1185
+
1186
+ const commerceS2Fr = {
1187
+ ...commerceS2En,
1188
+ column_blocks: [
1189
+ [
1190
+ {
1191
+ block_type: 'text',
1192
+ content: {
1193
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-emerald-500 sm:p-7">
1194
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"><svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="M3 10h18M7 15h3"></path></svg></div>
1195
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Checkout Stripe Headless</h4>
1196
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Création de sessions Stripe Checkout pour un ou plusieurs articles. Traitement automatique des commandes par webhooks avec protection d'idempotence.</p>
1197
+ </div>`,
1198
+ },
1199
+ },
1200
+ ],
1201
+ [
1202
+ {
1203
+ block_type: 'text',
1204
+ content: {
1205
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-emerald-500 sm:p-7">
1206
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"><svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 7a4 4 0 1 1-2.83 6.83L7 19H4v-3h3l2.17-2.17A4 4 0 0 1 15 7Z"></path><path d="M17.5 8.5h.01"></path></svg></div>
1207
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Licences Numériques Freemius</h4>
1208
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Distribuez vos logiciels, validez les clés de licence et gérez les périodes d'essai SaaS — aucune intégration tierce requise.</p>
1209
+ </div>`,
1210
+ },
1211
+ },
1212
+ ],
1213
+ [
1214
+ {
1215
+ block_type: 'text',
1216
+ content: {
1217
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-emerald-500 sm:p-7">
1218
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"><svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"></rect><path d="m4 15 4-4 4 4 3-3 5 5"></path><circle cx="15" cy="9" r="1"></circle></svg></div>
1219
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Merchandising Visuel</h4>
1220
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Vos équipes éditoriales enrichissent les fiches produits de landing pages, promotions et galeries — directement dans l'éditeur de blocs.</p>
1221
+ </div>`,
1222
+ },
1223
+ },
1224
+ ],
1225
+ ],
1226
+ };
1227
+
1228
+ const commerceS3Fr = {
1229
+ ...commerceS3En,
1230
+ column_blocks: [
1231
+ [
1232
+ {
1233
+ block_type: 'text',
1234
+ content: {
1235
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4">Sous le capot</p>
1236
+ <h3 class="text-2xl md:text-3xl font-extrabold text-white mb-4">Conçu pour la production à grande échelle</h3>
1237
+ <p class="text-slate-300 leading-relaxed mb-5">Commerce Pro a été conçu dès le départ pour les boutiques à fort trafic. Chaque API est mise en cache à la périphérie, chaque requête est indexée, et chaque gestionnaire de webhook est idempotent.</p>
1238
+ <ul class="space-y-3 text-sm text-slate-400">
1239
+ <li class="flex items-start gap-2.5">
1240
+ <span class="text-emerald-400">→</span>
1241
+ <span>Inventaire par variante avec verrouillage optimiste</span>
1242
+ </li>
1243
+ <li class="flex items-start gap-2.5">
1244
+ <span class="text-emerald-400">→</span>
1245
+ <span>Calcul automatique des zones d'expédition et tables de tarifs</span>
1246
+ </li>
1247
+ <li class="flex items-start gap-2.5">
1248
+ <span class="text-emerald-400">→</span>
1249
+ <span>Statut des commandes en temps réel via synchronisation Stripe webhook</span>
1250
+ </li>
1251
+ <li class="flex items-start gap-2.5">
1252
+ <span class="text-emerald-400">→</span>
1253
+ <span>Système d'attributs produits et de facettes filtrables</span>
1254
+ </li>
1255
+ </ul>`,
1256
+ },
1257
+ },
1258
+ ],
1259
+ [
1260
+ {
1261
+ block_type: 'text',
1262
+ content: {
1263
+ html_content: `<div class="space-y-4">
1264
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1265
+ <h5 class="text-sm font-bold text-white mb-1">Gestion des commandes</h5>
1266
+ <p class="text-xs text-slate-400 leading-relaxed">Cycle de vie complet : du panier à la livraison. Transitions automatiques, notifications par courriel et gestion des remboursements intégrées.</p>
1267
+ </div>
1268
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1269
+ <h5 class="text-sm font-bold text-white mb-1">Catégories de produits</h5>
1270
+ <p class="text-xs text-slate-400 leading-relaxed">Taxonomie hiérarchique avec slugs, médias associés et support i18n complet. Les catégories sont gérées directement dans le tableau de bord du CMS.</p>
1271
+ </div>
1272
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1273
+ <h5 class="text-sm font-bold text-white mb-1">Boutiques multilingues</h5>
1274
+ <p class="text-xs text-slate-400 leading-relaxed">Produits, catégories et flux de paiement entièrement traduisibles. Chaque variante linguistique partage l'inventaire et les prix tout en conservant ses propres métadonnées SEO.</p>
1275
+ </div>
1276
+ </div>`,
1277
+ },
1278
+ },
1279
+ ],
1280
+ ],
1281
+ };
1282
+
1283
+ const commerceS4Fr = {
1284
+ ...commerceS4En,
1285
+ column_blocks: [
1286
+ [
1287
+ {
1288
+ block_type: 'heading',
1289
+ content: { level: 2, text_content: 'Prêt à lancer votre boutique ?', textAlign: 'center', textColor: 'background' },
1290
+ },
1291
+ {
1292
+ block_type: 'text',
1293
+ content: {
1294
+ html_content: '<p class="text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6">Commencez à vendre dès aujourd\'hui avec Commerce Pro. Multi-devises, conforme aux taxes, ultra-rapide dès l\'installation.</p>',
1295
+ },
1296
+ },
1297
+ {
1298
+ block_type: 'button',
1299
+ content: {
1300
+ text: 'Acheter Commerce Pro',
1301
+ url: 'https://nextblock.dev/product/nextblock-commerce-pro-commerce-license',
1302
+ variant: 'secondary',
1303
+ size: 'lg',
1304
+ position: 'center',
1305
+ },
1306
+ },
1307
+ ],
1308
+ ],
1309
+ };
1310
+
1311
+ const commerceSectionsFr = [commerceS0Fr, commerceS1Fr, commerceS2Fr, commerceS3Fr, commerceS4Fr];
1312
+
1313
+ const [frProduct] = await params.sql`
1314
+ INSERT INTO public.products (
1315
+ sku,
1316
+ title,
1317
+ slug,
1318
+ price,
1319
+ sale_price,
1320
+ stock,
1321
+ status,
1322
+ short_description,
1323
+ description_json,
1324
+ product_type,
1325
+ payment_provider,
1326
+ language_id,
1327
+ translation_group_id,
1328
+ freemius_product_id,
1329
+ freemius_plan_id,
1330
+ trial_period_days,
1331
+ trial_requires_payment_method
1332
+ )
1333
+ VALUES (
1334
+ ${product.sku},
1335
+ 'Licence NextBlock™ Commerce Pro',
1336
+ ${String(product.slug) + '-fr'},
1337
+ ${product.price},
1338
+ ${product.sale_price},
1339
+ ${product.stock || 99},
1340
+ ${product.status},
1341
+ ${shortDescFr},
1342
+ NULL,
1343
+ 'digital',
1344
+ 'freemius',
1345
+ ${params.frLangId},
1346
+ ${product.translation_group_id},
1347
+ ${product.freemius_product_id},
1348
+ ${product.freemius_plan_id},
1349
+ ${product.trial_period_days ?? 0},
1350
+ ${product.trial_requires_payment_method ?? false}
1351
+ )
1352
+ ON CONFLICT ON CONSTRAINT products_language_id_slug_key DO UPDATE
1353
+ SET
1354
+ title = EXCLUDED.title,
1355
+ short_description = EXCLUDED.short_description,
1356
+ description_json = NULL,
1357
+ price = EXCLUDED.price,
1358
+ sale_price = EXCLUDED.sale_price,
1359
+ stock = EXCLUDED.stock,
1360
+ status = EXCLUDED.status,
1361
+ product_type = EXCLUDED.product_type,
1362
+ payment_provider = EXCLUDED.payment_provider,
1363
+ trial_period_days = EXCLUDED.trial_period_days,
1364
+ trial_requires_payment_method = EXCLUDED.trial_requires_payment_method
1365
+ RETURNING id
1366
+ `;
1367
+
1368
+ if (frProduct?.id) {
1369
+ await attachProductMedia(params.sql, frProduct.id as string, commerceMediaId);
1370
+ await params.sql`DELETE FROM public.blocks WHERE product_id = ${frProduct.id}`;
1371
+ for (let i = 0; i < commerceSectionsFr.length; i++) {
1372
+ await params.sql`
1373
+ INSERT INTO public.blocks (product_id, language_id, block_type, content, "order")
1374
+ VALUES (${frProduct.id}, ${params.frLangId}, 'section', ${params.sql.json(commerceSectionsFr[i] as any)}, ${i})
1375
+ `;
1376
+ }
1377
+ }
1378
+
1379
+ console.log('[Sandbox Reset] Successfully enriched commerce products (EN & FR).');
1380
+ }
1381
+
1382
+ async function enrichCortexAiProducts(params: {
1383
+ sql: SqlClient;
1384
+ cortexAsset: UploadedSeedAsset;
1385
+ enLangId: LanguageId;
1386
+ frLangId: LanguageId;
1387
+ }) {
1388
+ console.log('[Sandbox Reset] Enriching NextBlock™ Cortex AI...');
1389
+
1390
+ const cortexMediaId = await upsertMediaRecord(
1391
+ params.sql,
1392
+ params.cortexAsset,
1393
+ 'Sandbox seed asset: NextBlock™ Cortex AI.'
1394
+ );
1395
+
1396
+ const [product] = await params.sql`
1397
+ SELECT *
1398
+ FROM public.products
1399
+ WHERE freemius_product_id = ${SANDBOX_CORTEX_AI_PRODUCT_ID} AND language_id = ${params.enLangId}
1400
+ LIMIT 1
1401
+ `;
1402
+
1403
+ if (!product) {
1404
+ throw new Error(
1405
+ `Cortex AI product ${SANDBOX_CORTEX_AI_PRODUCT_ID} was not found after Freemius sync.`
1406
+ );
1407
+ }
1408
+
1409
+ const shortDescEn =
1410
+ 'NextBlock™ Cortex AI License brings block-level machine intelligence to your Next.js block editor. Generate copy, refactor structures, and automate translations in one click, built on an open, BYOK cost-controlled architecture.';
1411
+
1412
+ // ── Section 0: Hero Gradient ──
1413
+ const cortexS0En = {
1414
+ container_type: 'container',
1415
+ background: {
1416
+ type: 'gradient',
1417
+ gradient: {
1418
+ type: 'linear',
1419
+ direction: '135deg',
1420
+ stops: [
1421
+ { color: '#1e1b4b', position: 0 },
1422
+ { color: '#312e81', position: 35 },
1423
+ { color: '#0f172a', position: 100 },
1424
+ ],
1425
+ },
1426
+ },
1427
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 2 },
1428
+ column_gap: 'xl',
1429
+ vertical_alignment: 'center',
1430
+ padding: { top: 'xl', bottom: 'xl' },
1431
+ column_blocks: [
1432
+ [
1433
+ {
1434
+ block_type: 'text',
1435
+ content: {
1436
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4">AI Intelligence Layer</p>
1437
+ <h2 class="text-3xl md:text-5xl font-extrabold text-white leading-tight mb-5">Supercharge Your<br/>Editor with AI.</h2>
1438
+ <p class="text-base md:text-lg text-slate-200 leading-relaxed mb-6">Cortex AI integrates state-of-the-art LLMs directly into the block authoring surface. Generate copy, summarize content, refactor layouts, and translate pages — all without leaving your editor.</p>`,
1439
+ },
1440
+ },
1441
+ {
1442
+ block_type: 'button',
1443
+ content: {
1444
+ text: 'Get Cortex AI →',
1445
+ url: 'https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license',
1446
+ variant: 'default',
1447
+ size: 'lg',
1448
+ position: 'left',
1449
+ },
1450
+ },
1451
+ ],
1452
+ [
1453
+ {
1454
+ block_type: 'text',
1455
+ content: {
1456
+ html_content: `<div class="rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8">
1457
+ <h3 class="text-lg font-bold text-white mb-5">OpenRouter &amp; BYOK Architecture</h3>
1458
+ <ul class="space-y-4 text-sm leading-relaxed text-slate-300">
1459
+ <li class="flex items-start gap-3">
1460
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1461
+ <span><strong class="text-white">Bring Your Own Key</strong> — complete cost control using your own OpenRouter API tokens. No hidden fees.</span>
1462
+ </li>
1463
+ <li class="flex items-start gap-3">
1464
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1465
+ <span><strong class="text-white">Block-Aware Prompts</strong> — Cortex AI understands the JSONB schema, not just raw text. Outputs valid block structures.</span>
1466
+ </li>
1467
+ <li class="flex items-start gap-3">
1468
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1469
+ <span><strong class="text-white">Design-System Aware</strong> — generated content respects your Tailwind config and brand guidelines automatically.</span>
1470
+ </li>
1471
+ <li class="flex items-start gap-3">
1472
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1473
+ <span><strong class="text-white">Multi-Model Support</strong> — switch between GPT-4o, Claude, Gemini, and more via a single config toggle.</span>
1474
+ </li>
1475
+ </ul>
1476
+ </div>`,
1477
+ },
1478
+ },
1479
+ ],
1480
+ ],
1481
+ };
1482
+
1483
+ // ── Section 1: Social-proof metrics bar ──
1484
+ const cortexS1En = {
1485
+ container_type: 'container',
1486
+ background: { type: 'theme', theme: 'muted' },
1487
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 4 },
1488
+ column_gap: 'lg',
1489
+ padding: { top: 'lg', bottom: 'lg' },
1490
+ vertical_alignment: 'center',
1491
+ column_blocks: [
1492
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">50+</span><span class="text-xs text-muted-foreground uppercase tracking-wider">AI Models Available</span></p>' } }],
1493
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">< 2s</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Avg. Generation Time</span></p>' } }],
1494
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">100 %</span><span class="text-xs text-muted-foreground uppercase tracking-wider">BYOK Cost Control</span></p>' } }],
1495
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">2</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Languages Supported</span></p>' } }],
1496
+ ],
1497
+ };
1498
+
1499
+ // ── Section 2: Three-column feature cards ──
1500
+ const cortexS2En = {
1501
+ container_type: 'container',
1502
+ background: { type: 'none' },
1503
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
1504
+ column_gap: 'lg',
1505
+ padding: { top: 'xl', bottom: 'xl' },
1506
+ vertical_alignment: 'stretch',
1507
+ column_blocks: [
1508
+ [
1509
+ {
1510
+ block_type: 'text',
1511
+ content: {
1512
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7">
1513
+ <p class="text-2xl mb-3">✍️</p>
1514
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">One-Click Copywriting</h4>
1515
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Draft blog entries, optimize headlines, generate call-to-actions, and write product descriptions — all with context-aware prompts that know your content.</p>
1516
+ </div>`,
1517
+ },
1518
+ },
1519
+ ],
1520
+ [
1521
+ {
1522
+ block_type: 'text',
1523
+ content: {
1524
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7">
1525
+ <p class="text-2xl mb-3">🔄</p>
1526
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Structure Refactoring</h4>
1527
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Instantly convert columns, add layout grids, or reorganize block nodes. Cortex AI generates valid JSONB schema, not just text suggestions.</p>
1528
+ </div>`,
1529
+ },
1530
+ },
1531
+ ],
1532
+ [
1533
+ {
1534
+ block_type: 'text',
1535
+ content: {
1536
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7">
1537
+ <p class="text-2xl mb-3">🌐</p>
1538
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Automated Translation</h4>
1539
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Localize complete pages between English and French while preserving all nested sub-blocks, layouts, and section styling intact.</p>
1540
+ </div>`,
1541
+ },
1542
+ },
1543
+ ],
1544
+ ],
1545
+ };
1546
+
1547
+ // ── Section 3: Deep-dive 2-col ──
1548
+ const cortexS3En = {
1549
+ container_type: 'container',
1550
+ background: {
1551
+ type: 'gradient',
1552
+ gradient: {
1553
+ type: 'linear',
1554
+ direction: '180deg',
1555
+ stops: [
1556
+ { color: '#020617', position: 0 },
1557
+ { color: '#0f172a', position: 100 },
1558
+ ],
1559
+ },
1560
+ },
1561
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 2 },
1562
+ column_gap: 'xl',
1563
+ vertical_alignment: 'center',
1564
+ padding: { top: 'xl', bottom: 'xl' },
1565
+ column_blocks: [
1566
+ [
1567
+ {
1568
+ block_type: 'text',
1569
+ content: {
1570
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4">How It Works</p>
1571
+ <h3 class="text-2xl md:text-3xl font-extrabold text-white mb-4">AI That Understands Blocks</h3>
1572
+ <p class="text-slate-300 leading-relaxed mb-5">Unlike generic AI tools, Cortex AI is deeply integrated with the NextBlock™ block editor. It understands your section layouts, column structures, and nested content hierarchies — generating outputs that slot directly into your page without manual cleanup.</p>
1573
+ <ul class="space-y-3 text-sm text-slate-400">
1574
+ <li class="flex items-start gap-2.5">
1575
+ <span class="text-violet-400">→</span>
1576
+ <span>Inline AI toolbar appears on text selection</span>
1577
+ </li>
1578
+ <li class="flex items-start gap-2.5">
1579
+ <span class="text-violet-400">→</span>
1580
+ <span>Prompt presets for common content tasks</span>
1581
+ </li>
1582
+ <li class="flex items-start gap-2.5">
1583
+ <span class="text-violet-400">→</span>
1584
+ <span>Full-page generation from a single prompt</span>
1585
+ </li>
1586
+ <li class="flex items-start gap-2.5">
1587
+ <span class="text-violet-400">→</span>
1588
+ <span>Token usage tracking in the admin dashboard</span>
1589
+ </li>
1590
+ </ul>`,
1591
+ },
1592
+ },
1593
+ ],
1594
+ [
1595
+ {
1596
+ block_type: 'text',
1597
+ content: {
1598
+ html_content: `<div class="space-y-4">
1599
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1600
+ <h5 class="text-sm font-bold text-white mb-1">Content Generation</h5>
1601
+ <p class="text-xs text-slate-400 leading-relaxed">From short ad copy to long-form articles, Cortex AI generates on-brand content that matches your tone and style. Outputs are pre-formatted for your design system.</p>
1602
+ </div>
1603
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1604
+ <h5 class="text-sm font-bold text-white mb-1">SEO Optimization</h5>
1605
+ <p class="text-xs text-slate-400 leading-relaxed">Auto-generate meta titles, descriptions, and heading hierarchies. Cortex AI analyzes your content structure and suggests SEO improvements in real-time.</p>
1606
+ </div>
1607
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1608
+ <h5 class="text-sm font-bold text-white mb-1">Privacy-First Design</h5>
1609
+ <p class="text-xs text-slate-400 leading-relaxed">Your content is sent directly to OpenRouter using YOUR key. NextBlock™ never stores, logs, or proxies your AI requests — full data sovereignty guaranteed.</p>
1610
+ </div>
1611
+ </div>`,
1612
+ },
1613
+ },
1614
+ ],
1615
+ ],
1616
+ };
1617
+
1618
+ // ── Section 4: CTA ──
1619
+ const cortexS4En = {
1620
+ container_type: 'container',
1621
+ background: {
1622
+ type: 'gradient',
1623
+ gradient: {
1624
+ type: 'linear',
1625
+ direction: '135deg',
1626
+ stops: [
1627
+ { color: '#312e81', position: 0 },
1628
+ { color: '#1e1b4b', position: 100 },
1629
+ ],
1630
+ },
1631
+ },
1632
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
1633
+ column_gap: 'none',
1634
+ padding: { top: 'xl', bottom: 'xl' },
1635
+ vertical_alignment: 'center',
1636
+ column_blocks: [
1637
+ [
1638
+ {
1639
+ block_type: 'heading',
1640
+ content: { level: 2, text_content: 'Ready to add AI to your editor?', textAlign: 'center', textColor: 'background' },
1641
+ },
1642
+ {
1643
+ block_type: 'text',
1644
+ content: {
1645
+ html_content: '<p class="text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6">Unlock the full power of AI-driven content creation. BYOK, privacy-first, and deeply integrated with your block editor.</p>',
1646
+ },
1647
+ },
1648
+ {
1649
+ block_type: 'button',
1650
+ content: {
1651
+ text: 'Purchase Cortex AI',
1652
+ url: 'https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license',
1653
+ variant: 'secondary',
1654
+ size: 'lg',
1655
+ position: 'center',
1656
+ },
1657
+ },
1658
+ ],
1659
+ ],
1660
+ };
1661
+
1662
+ const cortexSectionsEn = [cortexS0En, cortexS1En, cortexS2En, cortexS3En, cortexS4En];
1663
+
1664
+ await params.sql`
1665
+ UPDATE public.products
1666
+ SET
1667
+ title = 'NextBlock™ Cortex AI - Cortex AI License',
1668
+ short_description = ${shortDescEn},
1669
+ description_json = NULL,
1670
+ product_type = 'digital',
1671
+ payment_provider = 'freemius'
1672
+ WHERE id = ${product.id}
1673
+ `;
1674
+
1675
+ await attachProductMedia(params.sql, product.id as string, cortexMediaId);
1676
+
1677
+ // Set description blocks for English Cortex AI product
1678
+ await params.sql`DELETE FROM public.blocks WHERE product_id = ${product.id}`;
1679
+ for (let i = 0; i < cortexSectionsEn.length; i++) {
1680
+ await params.sql`
1681
+ INSERT INTO public.blocks (product_id, language_id, block_type, content, "order")
1682
+ VALUES (${product.id}, ${params.enLangId}, 'section', ${params.sql.json(cortexSectionsEn[i] as any)}, ${i})
1683
+ `;
1684
+ }
1685
+
1686
+ // ── French Sections ──
1687
+ const shortDescFr =
1688
+ "La licence NextBlock™ Cortex AI apporte l'intelligence artificielle au niveau des blocs directement dans votre éditeur de contenu Next.js. Génération, refactorisation et traduction de pages en un clic.";
1689
+
1690
+ const cortexS0Fr = {
1691
+ ...cortexS0En,
1692
+ column_blocks: [
1693
+ [
1694
+ {
1695
+ block_type: 'text',
1696
+ content: {
1697
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4">Couche d'Intelligence IA</p>
1698
+ <h2 class="text-3xl md:text-5xl font-extrabold text-white leading-tight mb-5">Boostez votre éditeur<br/>avec l'IA native.</h2>
1699
+ <p class="text-base md:text-lg text-slate-200 leading-relaxed mb-6">Cortex AI intègre les LLMs les plus performants directement dans votre surface d'édition de blocs. Rédigez, résumez, restructurez et traduisez — sans quitter l'éditeur.</p>`,
1700
+ },
1701
+ },
1702
+ {
1703
+ block_type: 'button',
1704
+ content: {
1705
+ text: 'Obtenir Cortex AI →',
1706
+ url: 'https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license',
1707
+ variant: 'default',
1708
+ size: 'lg',
1709
+ position: 'left',
1710
+ },
1711
+ },
1712
+ ],
1713
+ [
1714
+ {
1715
+ block_type: 'text',
1716
+ content: {
1717
+ html_content: `<div class="rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8">
1718
+ <h3 class="text-lg font-bold text-white mb-5">OpenRouter et architecture BYOK</h3>
1719
+ <ul class="space-y-4 text-sm leading-relaxed text-slate-300">
1720
+ <li class="flex items-start gap-3">
1721
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1722
+ <span><strong class="text-white">Bring Your Own Key</strong> — contrôle total des coûts avec vos propres jetons API OpenRouter. Aucun frais caché.</span>
1723
+ </li>
1724
+ <li class="flex items-start gap-3">
1725
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1726
+ <span><strong class="text-white">Prompts conscients des blocs</strong> — Cortex AI comprend le schéma JSONB, pas seulement le texte brut. Produit des structures de blocs valides.</span>
1727
+ </li>
1728
+ <li class="flex items-start gap-3">
1729
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1730
+ <span><strong class="text-white">Respect du design system</strong> — le contenu généré respecte automatiquement votre configuration Tailwind et vos guidelines de marque.</span>
1731
+ </li>
1732
+ <li class="flex items-start gap-3">
1733
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold">✓</span>
1734
+ <span><strong class="text-white">Support multi-modèles</strong> — basculez entre GPT-4o, Claude, Gemini et plus via un simple paramètre.</span>
1735
+ </li>
1736
+ </ul>
1737
+ </div>`,
1738
+ },
1739
+ },
1740
+ ],
1741
+ ],
1742
+ };
1743
+
1744
+ const cortexS1Fr = {
1745
+ ...cortexS1En,
1746
+ column_blocks: [
1747
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">50+</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Modèles IA disponibles</span></p>' } }],
1748
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">< 2s</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Temps de génération moy.</span></p>' } }],
1749
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">100 %</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Contrôle des coûts BYOK</span></p>' } }],
1750
+ [{ block_type: 'text', content: { html_content: '<p class="text-center"><span class="block text-2xl font-extrabold text-foreground">2</span><span class="text-xs text-muted-foreground uppercase tracking-wider">Langues supportées</span></p>' } }],
1751
+ ],
1752
+ };
1753
+
1754
+ const cortexS2Fr = {
1755
+ ...cortexS2En,
1756
+ column_blocks: [
1757
+ [
1758
+ {
1759
+ block_type: 'text',
1760
+ content: {
1761
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7">
1762
+ <p class="text-2xl mb-3">✍️</p>
1763
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Rédaction en un clic</h4>
1764
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Rédigez des articles, optimisez vos titres, créez des appels à l'action et rédigez des descriptions produits — le tout avec des prompts contextuels.</p>
1765
+ </div>`,
1766
+ },
1767
+ },
1768
+ ],
1769
+ [
1770
+ {
1771
+ block_type: 'text',
1772
+ content: {
1773
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7">
1774
+ <p class="text-2xl mb-3">🔄</p>
1775
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Refactorisation de structure</h4>
1776
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Convertissez des colonnes, ajoutez des grilles ou réorganisez vos nœuds de blocs. Cortex AI génère un schéma JSONB valide, pas des suggestions textuelles.</p>
1777
+ </div>`,
1778
+ },
1779
+ },
1780
+ ],
1781
+ [
1782
+ {
1783
+ block_type: 'text',
1784
+ content: {
1785
+ html_content: `<div class="h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7">
1786
+ <p class="text-2xl mb-3">🌐</p>
1787
+ <h4 class="text-base font-bold text-slate-900 dark:text-white mb-2">Traduction automatique</h4>
1788
+ <p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">Localisez vos pages complètes entre le français et l'anglais en préservant tous les blocs imbriqués, mises en page et styles de section.</p>
1789
+ </div>`,
1790
+ },
1791
+ },
1792
+ ],
1793
+ ],
1794
+ };
1795
+
1796
+ const cortexS3Fr = {
1797
+ ...cortexS3En,
1798
+ column_blocks: [
1799
+ [
1800
+ {
1801
+ block_type: 'text',
1802
+ content: {
1803
+ html_content: `<p class="text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4">Comment ça marche</p>
1804
+ <h3 class="text-2xl md:text-3xl font-extrabold text-white mb-4">Une IA qui comprend les blocs</h3>
1805
+ <p class="text-slate-300 leading-relaxed mb-5">Contrairement aux outils IA génériques, Cortex AI est profondément intégré à l'éditeur de blocs NextBlock™. Il comprend vos sections, structures de colonnes et hiérarchies de contenu — générant des sorties qui s'insèrent directement dans votre page.</p>
1806
+ <ul class="space-y-3 text-sm text-slate-400">
1807
+ <li class="flex items-start gap-2.5">
1808
+ <span class="text-violet-400">→</span>
1809
+ <span>Barre d'outils IA intégrée à la sélection de texte</span>
1810
+ </li>
1811
+ <li class="flex items-start gap-2.5">
1812
+ <span class="text-violet-400">→</span>
1813
+ <span>Prompts prédéfinis pour les tâches courantes</span>
1814
+ </li>
1815
+ <li class="flex items-start gap-2.5">
1816
+ <span class="text-violet-400">→</span>
1817
+ <span>Génération de pages complètes à partir d'un seul prompt</span>
1818
+ </li>
1819
+ <li class="flex items-start gap-2.5">
1820
+ <span class="text-violet-400">→</span>
1821
+ <span>Suivi de la consommation de tokens dans le tableau de bord</span>
1822
+ </li>
1823
+ </ul>`,
1824
+ },
1825
+ },
1826
+ ],
1827
+ [
1828
+ {
1829
+ block_type: 'text',
1830
+ content: {
1831
+ html_content: `<div class="space-y-4">
1832
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1833
+ <h5 class="text-sm font-bold text-white mb-1">Génération de contenu</h5>
1834
+ <p class="text-xs text-slate-400 leading-relaxed">Des textes publicitaires aux articles de fond, Cortex AI génère du contenu fidèle à votre marque. Les sorties sont pré-formatées pour votre design system.</p>
1835
+ </div>
1836
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1837
+ <h5 class="text-sm font-bold text-white mb-1">Optimisation SEO</h5>
1838
+ <p class="text-xs text-slate-400 leading-relaxed">Génération automatique de titres méta, descriptions et hiérarchie de titres. Cortex AI analyse votre structure et suggère des améliorations SEO en temps réel.</p>
1839
+ </div>
1840
+ <div class="p-5 rounded-xl border border-slate-700 bg-slate-900">
1841
+ <h5 class="text-sm font-bold text-white mb-1">Conception axée sur la vie privée</h5>
1842
+ <p class="text-xs text-slate-400 leading-relaxed">Votre contenu est envoyé directement à OpenRouter avec VOTRE clé. NextBlock™ ne stocke, ne journalise et ne proxifie jamais vos requêtes IA — souveraineté totale sur vos données.</p>
1843
+ </div>
1844
+ </div>`,
1845
+ },
1846
+ },
1847
+ ],
1848
+ ],
1849
+ };
1850
+
1851
+ const cortexS4Fr = {
1852
+ ...cortexS4En,
1853
+ column_blocks: [
1854
+ [
1855
+ {
1856
+ block_type: 'heading',
1857
+ content: { level: 2, text_content: "Prêt à ajouter l'IA à votre éditeur ?", textAlign: 'center', textColor: 'background' },
1858
+ },
1859
+ {
1860
+ block_type: 'text',
1861
+ content: {
1862
+ html_content: '<p class="text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6">Libérez la puissance de la création de contenu assistée par IA. BYOK, vie privée d\'abord, et intégration profonde avec votre éditeur de blocs.</p>',
1863
+ },
1864
+ },
1865
+ {
1866
+ block_type: 'button',
1867
+ content: {
1868
+ text: 'Acheter Cortex AI',
1869
+ url: 'https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license',
1870
+ variant: 'secondary',
1871
+ size: 'lg',
1872
+ position: 'center',
1873
+ },
1874
+ },
1875
+ ],
1876
+ ],
1877
+ };
1878
+
1879
+ const cortexSectionsFr = [cortexS0Fr, cortexS1Fr, cortexS2Fr, cortexS3Fr, cortexS4Fr];
1880
+
1881
+ const [frProduct] = await params.sql`
1882
+ INSERT INTO public.products (
1883
+ sku, title, slug, price, sale_price, stock, status,
1884
+ short_description, description_json,
1885
+ product_type, payment_provider,
1886
+ language_id, translation_group_id,
1887
+ freemius_product_id, freemius_plan_id,
1888
+ trial_period_days, trial_requires_payment_method
1889
+ )
1890
+ VALUES (
1891
+ ${product.sku}, 'Licence NextBlock™ Cortex AI', ${String(product.slug) + '-fr'},
1892
+ ${product.price}, ${product.sale_price}, ${product.stock || 99}, ${product.status},
1893
+ ${shortDescFr}, NULL,
1894
+ 'digital', 'freemius',
1895
+ ${params.frLangId}, ${product.translation_group_id},
1896
+ ${product.freemius_product_id}, ${product.freemius_plan_id},
1897
+ ${product.trial_period_days ?? 0}, ${product.trial_requires_payment_method ?? false}
1898
+ )
1899
+ ON CONFLICT ON CONSTRAINT products_language_id_slug_key DO UPDATE
1900
+ SET
1901
+ title = EXCLUDED.title,
1902
+ short_description = EXCLUDED.short_description,
1903
+ description_json = NULL,
1904
+ product_type = EXCLUDED.product_type,
1905
+ payment_provider = EXCLUDED.payment_provider,
1906
+ trial_period_days = EXCLUDED.trial_period_days,
1907
+ trial_requires_payment_method = EXCLUDED.trial_requires_payment_method
1908
+ RETURNING id
1909
+ `;
1910
+
1911
+ if (frProduct?.id) {
1912
+ await attachProductMedia(params.sql, frProduct.id as string, cortexMediaId);
1913
+ await params.sql`DELETE FROM public.blocks WHERE product_id = ${frProduct.id}`;
1914
+ for (let i = 0; i < cortexSectionsFr.length; i++) {
1915
+ await params.sql`
1916
+ INSERT INTO public.blocks (product_id, language_id, block_type, content, "order")
1917
+ VALUES (${frProduct.id}, ${params.frLangId}, 'section', ${params.sql.json(cortexSectionsFr[i] as any)}, ${i})
1918
+ `;
1919
+ }
1920
+ }
1921
+
1922
+ console.log('[Sandbox Reset] Successfully enriched Cortex AI products (EN & FR).');
1923
+ }
1924
+
1925
+
1926
+ async function ensureSandboxCommerceProductSynced(params: {
1927
+ sql: SqlClient;
1928
+ enLangId: LanguageId;
1929
+ }) {
1930
+ const [existingProduct] = await params.sql`
1931
+ SELECT id
1932
+ FROM public.products
1933
+ WHERE freemius_product_id = ${SANDBOX_COMMERCE_PRODUCT_ID}
1934
+ AND language_id = ${params.enLangId}
1935
+ LIMIT 1
1936
+ `;
1937
+
1938
+ if (existingProduct?.id) {
1939
+ return existingProduct.id as string;
1940
+ }
1941
+
1942
+ console.warn(
1943
+ `[Sandbox Reset] Commerce Pro product ${SANDBOX_COMMERCE_PRODUCT_ID} was missing after the full Freemius sync. Retrying targeted sync.`
1944
+ );
1945
+
1946
+ const fallbackResult = await syncSingleFreemiusProduct(SANDBOX_COMMERCE_PRODUCT_ID);
1947
+ console.log(
1948
+ `[Sandbox Reset] Targeted Commerce Pro sync completed with ${fallbackResult?.count || 0} product(s).`
1949
+ );
1950
+
1951
+ const [syncedProduct] = await params.sql`
1952
+ SELECT id
1953
+ FROM public.products
1954
+ WHERE freemius_product_id = ${SANDBOX_COMMERCE_PRODUCT_ID}
1955
+ AND language_id = ${params.enLangId}
1956
+ LIMIT 1
1957
+ `;
1958
+
1959
+ if (!syncedProduct?.id) {
1960
+ throw new Error(
1961
+ `Targeted Commerce Pro sync did not create product ${SANDBOX_COMMERCE_PRODUCT_ID}.`
1962
+ );
1963
+ }
1964
+
1965
+ return syncedProduct.id as string;
1966
+ }
1967
+
1968
+ async function upsertSeededCatalogProduct(params: {
1969
+ sql: SqlClient;
1970
+ productId?: string;
1971
+ translationGroupId: string;
1972
+ languageId: LanguageId;
1973
+ localeCode: 'en' | 'fr';
1974
+ locale: SeededLocale;
1975
+ baseSku: string;
1976
+ price: number;
1977
+ accent: ApparelAccentName;
1978
+ mediaId: string;
1979
+ variantStocks: Record<SizeSlug, number>;
1980
+ sizeTermIds: Record<SizeSlug, string>;
1981
+ }) {
1982
+ const variantDefinitions = [
1983
+ { slug: 'small', skuSuffix: 'S' },
1984
+ { slug: 'medium', skuSuffix: 'M' },
1985
+ { slug: 'large', skuSuffix: 'L' },
1986
+ ] as const;
1987
+
1988
+ const totalStock = variantDefinitions.reduce(
1989
+ (sum, variant) => sum + params.variantStocks[variant.slug],
1990
+ 0
1991
+ );
1992
+
1993
+ const metadata = {
1994
+ seed_source: 'sandbox-reset',
1995
+ seed_type: 'physical-apparel',
1996
+ };
1997
+ const descriptionSections = buildApparelDescriptionSections(
1998
+ params.locale.description,
1999
+ params.accent,
2000
+ params.localeCode
2001
+ );
2002
+
2003
+ let seededProductId = params.productId;
2004
+
2005
+ if (seededProductId) {
2006
+ const [updatedProduct] = await params.sql`
2007
+ UPDATE public.products
2008
+ SET
2009
+ language_id = ${params.languageId},
2010
+ translation_group_id = ${params.translationGroupId},
2011
+ sku = ${params.baseSku},
2012
+ title = ${params.locale.title},
2013
+ slug = ${params.locale.slug},
2014
+ price = ${params.price},
2015
+ sale_price = NULL,
2016
+ stock = ${totalStock},
2017
+ status = 'active',
2018
+ short_description = ${params.locale.shortDescription},
2019
+ description_json = NULL,
2020
+ metadata = ${params.sql.json(metadata)},
2021
+ is_taxable = true,
2022
+ product_type = 'physical',
2023
+ payment_provider = 'stripe',
2024
+ trial_period_days = 0,
2025
+ trial_requires_payment_method = false,
2026
+ updated_at = now()
2027
+ WHERE id = ${seededProductId}
2028
+ RETURNING id
2029
+ `;
2030
+
2031
+ seededProductId = updatedProduct?.id as string | undefined;
2032
+ }
2033
+
2034
+ if (!seededProductId) {
2035
+ const [upsertedProduct] = await params.sql`
2036
+ INSERT INTO public.products (
2037
+ language_id,
2038
+ translation_group_id,
2039
+ sku,
2040
+ title,
2041
+ slug,
2042
+ price,
2043
+ sale_price,
2044
+ stock,
2045
+ status,
2046
+ short_description,
2047
+ description_json,
2048
+ metadata,
2049
+ is_taxable,
2050
+ product_type,
2051
+ payment_provider,
2052
+ trial_period_days,
2053
+ trial_requires_payment_method
2054
+ )
2055
+ VALUES (
2056
+ ${params.languageId},
2057
+ ${params.translationGroupId},
2058
+ ${params.baseSku},
2059
+ ${params.locale.title},
2060
+ ${params.locale.slug},
2061
+ ${params.price},
2062
+ NULL,
2063
+ ${totalStock},
2064
+ 'active',
2065
+ ${params.locale.shortDescription},
2066
+ NULL,
2067
+ ${params.sql.json(metadata)},
2068
+ true,
2069
+ 'physical',
2070
+ 'stripe',
2071
+ 0,
2072
+ false
2073
+ )
2074
+ ON CONFLICT ON CONSTRAINT products_language_id_slug_key DO UPDATE
2075
+ SET
2076
+ translation_group_id = EXCLUDED.translation_group_id,
2077
+ sku = EXCLUDED.sku,
2078
+ title = EXCLUDED.title,
2079
+ price = EXCLUDED.price,
2080
+ sale_price = EXCLUDED.sale_price,
2081
+ stock = EXCLUDED.stock,
2082
+ status = EXCLUDED.status,
2083
+ short_description = EXCLUDED.short_description,
2084
+ description_json = NULL,
2085
+ metadata = EXCLUDED.metadata,
2086
+ is_taxable = EXCLUDED.is_taxable,
2087
+ product_type = EXCLUDED.product_type,
2088
+ payment_provider = EXCLUDED.payment_provider,
2089
+ trial_period_days = EXCLUDED.trial_period_days,
2090
+ trial_requires_payment_method = EXCLUDED.trial_requires_payment_method,
2091
+ updated_at = now()
2092
+ RETURNING id
2093
+ `;
2094
+
2095
+ seededProductId = upsertedProduct?.id as string | undefined;
2096
+ }
2097
+
2098
+ if (!seededProductId) {
2099
+ throw new Error(`Failed to upsert seeded product ${params.locale.slug}.`);
2100
+ }
2101
+
2102
+ await attachProductMedia(params.sql, seededProductId, params.mediaId);
2103
+
2104
+ // Set description blocks (rich section layout, editable in the block editor)
2105
+ await params.sql`DELETE FROM public.blocks WHERE product_id = ${seededProductId}`;
2106
+ for (let i = 0; i < descriptionSections.length; i++) {
2107
+ await params.sql`
2108
+ INSERT INTO public.blocks (product_id, language_id, block_type, content, "order")
2109
+ VALUES (${seededProductId}, ${params.languageId}, 'section', ${params.sql.json(descriptionSections[i] as any)}, ${i})
2110
+ `;
2111
+ }
2112
+
2113
+ await params.sql`
2114
+ DELETE FROM public.variant_attribute_mapping
2115
+ WHERE variant_id IN (
2116
+ SELECT id
2117
+ FROM public.product_variants
2118
+ WHERE product_id = ${seededProductId}
2119
+ )
2120
+ `;
2121
+
2122
+ await params.sql`
2123
+ DELETE FROM public.product_variants
2124
+ WHERE product_id = ${seededProductId}
2125
+ `;
2126
+
2127
+ for (const variant of variantDefinitions) {
2128
+ const [insertedVariant] = await params.sql`
2129
+ INSERT INTO public.product_variants (
2130
+ product_id,
2131
+ sku,
2132
+ price,
2133
+ sale_price,
2134
+ stock_quantity,
2135
+ main_media_id
2136
+ )
2137
+ VALUES (
2138
+ ${seededProductId},
2139
+ ${`${params.baseSku}-${variant.skuSuffix}`},
2140
+ ${params.price},
2141
+ NULL,
2142
+ ${params.variantStocks[variant.slug]},
2143
+ ${params.mediaId}
2144
+ )
2145
+ RETURNING id
2146
+ `;
2147
+
2148
+ if (!insertedVariant?.id) {
2149
+ throw new Error(`Failed to create variant ${params.baseSku}-${variant.skuSuffix}.`);
2150
+ }
2151
+
2152
+ await params.sql`
2153
+ INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)
2154
+ VALUES (${insertedVariant.id}, ${params.sizeTermIds[variant.slug]})
2155
+ `;
2156
+ }
2157
+
2158
+ await params.sql`
2159
+ UPDATE public.product_variants
2160
+ SET
2161
+ main_media_id = ${params.mediaId},
2162
+ updated_at = now()
2163
+ WHERE product_id = ${seededProductId}
2164
+ `;
2165
+
2166
+ await upsertInventoryItems(
2167
+ params.sql,
2168
+ variantDefinitions.map((variant) => ({
2169
+ sku: `${params.baseSku}-${variant.skuSuffix}`,
2170
+ quantity: params.variantStocks[variant.slug],
2171
+ }))
2172
+ );
2173
+
2174
+ return seededProductId;
2175
+ }
2176
+
2177
+ async function seedApparelCatalog(params: {
2178
+ sql: SqlClient;
2179
+ enLangId: LanguageId;
2180
+ frLangId: LanguageId;
2181
+ uploadedAssets: Map<string, UploadedSeedAsset>;
2182
+ }) {
2183
+ console.log('[Sandbox Reset] Seeding apparel catalog...');
2184
+
2185
+ const sizeTermIds = await ensureSizeAttribute(params.sql);
2186
+
2187
+ for (const productSeed of APPAREL_PRODUCT_SEEDS) {
2188
+ const uploadedAsset = params.uploadedAssets.get(productSeed.imageKey);
2189
+ if (!uploadedAsset) {
2190
+ throw new Error(`Missing uploaded asset for ${productSeed.imageKey}.`);
2191
+ }
2192
+
2193
+ const mediaId = await upsertMediaRecord(params.sql, uploadedAsset, uploadedAsset.description);
2194
+
2195
+ const [existingEnProduct] = await params.sql`
2196
+ SELECT id, translation_group_id
2197
+ FROM public.products
2198
+ WHERE language_id = ${params.enLangId} AND slug = ${productSeed.en.slug}
2199
+ LIMIT 1
2200
+ `;
2201
+
2202
+ const [existingFrProduct] = await params.sql`
2203
+ SELECT id, translation_group_id
2204
+ FROM public.products
2205
+ WHERE language_id = ${params.frLangId} AND slug = ${productSeed.fr.slug}
2206
+ LIMIT 1
2207
+ `;
2208
+
2209
+ const translationGroupId =
2210
+ (existingEnProduct?.translation_group_id as string | undefined) ||
2211
+ (existingFrProduct?.translation_group_id as string | undefined) ||
2212
+ crypto.randomUUID();
2213
+
2214
+ await upsertSeededCatalogProduct({
2215
+ sql: params.sql,
2216
+ productId: existingEnProduct?.id as string | undefined,
2217
+ translationGroupId,
2218
+ languageId: params.enLangId,
2219
+ localeCode: 'en',
2220
+ locale: productSeed.en,
2221
+ baseSku: productSeed.baseSku,
2222
+ price: productSeed.price,
2223
+ accent: productSeed.accent,
2224
+ mediaId,
2225
+ variantStocks: productSeed.variantStocks,
2226
+ sizeTermIds,
2227
+ });
2228
+
2229
+ await upsertSeededCatalogProduct({
2230
+ sql: params.sql,
2231
+ productId: existingFrProduct?.id as string | undefined,
2232
+ translationGroupId,
2233
+ languageId: params.frLangId,
2234
+ localeCode: 'fr',
2235
+ locale: productSeed.fr,
2236
+ baseSku: productSeed.baseSku,
2237
+ price: productSeed.price,
2238
+ accent: productSeed.accent,
2239
+ mediaId,
2240
+ variantStocks: productSeed.variantStocks,
2241
+ sizeTermIds,
2242
+ });
2243
+ }
2244
+
2245
+ console.log('[Sandbox Reset] Successfully seeded apparel catalog.');
2246
+ }
2247
+
2248
+ async function ensureShopPagesAndNavigation(params: {
2249
+ sql: SqlClient;
2250
+ enLangId: LanguageId;
2251
+ frLangId: LanguageId;
2252
+ }) {
2253
+ console.log('[Sandbox Reset] Adding Shop Pages and navigation items...');
2254
+ let globalShopGroupId: string | undefined;
2255
+
2256
+ {
2257
+ const langId = params.enLangId;
2258
+ const [existingPage] = await params.sql`
2259
+ SELECT id, translation_group_id
2260
+ FROM public.pages
2261
+ WHERE language_id = ${langId} AND slug = 'shop'
2262
+ `;
2263
+ let pageId = existingPage?.id as number | undefined;
2264
+ globalShopGroupId = existingPage?.translation_group_id as string | undefined;
2265
+
2266
+ if (!pageId) {
2267
+ const [newPage] = await params.sql`
2268
+ INSERT INTO public.pages (language_id, title, slug, status, meta_title, meta_description)
2269
+ VALUES (
2270
+ ${langId},
2271
+ 'Shop Our Products',
2272
+ 'shop',
2273
+ 'published',
2274
+ 'NextBlock™ Store',
2275
+ 'Browse our premium products'
2276
+ )
2277
+ RETURNING id, translation_group_id
2278
+ `;
2279
+ pageId = newPage.id as number;
2280
+ globalShopGroupId = newPage?.translation_group_id as string | undefined;
2281
+
2282
+ const heroContent = {
2283
+ is_hero: true,
2284
+ container_type: 'full-width',
2285
+ background: {
2286
+ type: 'theme',
2287
+ theme: 'primary',
2288
+ },
2289
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
2290
+ column_gap: 'lg',
2291
+ padding: { top: 'xl', bottom: 'xl' },
2292
+ vertical_alignment: 'center',
2293
+ column_blocks: [
2294
+ [
2295
+ {
2296
+ block_type: 'heading',
2297
+ content: {
2298
+ level: 1,
2299
+ text_content: 'NextBlock™ Store',
2300
+ textAlign: 'center',
2301
+ textColor: 'background',
2302
+ },
2303
+ },
2304
+ {
2305
+ block_type: 'text',
2306
+ content: {
2307
+ html_content:
2308
+ '<p style="text-align: center; color: var(--background); opacity: 0.9">Discover our premium selection of developer tools and digital commerce solutions.</p>',
2309
+ },
2310
+ },
2311
+ ],
2312
+ ],
2313
+ };
2314
+
2315
+ const sectionContent = {
2316
+ container_type: 'container',
2317
+ background: { type: 'none' },
2318
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
2319
+ column_gap: 'none',
2320
+ padding: { top: 'xl', bottom: 'xl' },
2321
+ column_blocks: [
2322
+ [
2323
+ {
2324
+ block_type: 'heading',
2325
+ content: {
2326
+ level: 2,
2327
+ text_content: 'Featured Products',
2328
+ textAlign: 'center',
2329
+ },
2330
+ },
2331
+ {
2332
+ block_type: 'product_grid',
2333
+ content: {
2334
+ type: 'latest',
2335
+ limit: 6,
2336
+ },
2337
+ },
2338
+ ],
2339
+ ],
2340
+ };
2341
+
2342
+ await params.sql`
2343
+ INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
2344
+ VALUES
2345
+ (${pageId}, ${langId}, 'section', ${params.sql.json(heroContent as any)}, 0),
2346
+ (${pageId}, ${langId}, 'section', ${params.sql.json(sectionContent as any)}, 1)
2347
+ `;
2348
+ }
2349
+
2350
+ const [exists] = await params.sql`
2351
+ SELECT id
2352
+ FROM public.navigation_items
2353
+ WHERE language_id = ${langId} AND url = '/shop'
2354
+ `;
2355
+ if (!exists) {
2356
+ await params.sql`
2357
+ INSERT INTO public.navigation_items (language_id, menu_key, label, url, "order")
2358
+ VALUES (${langId}, 'HEADER', 'Shop', '/shop', 2)
2359
+ `;
2360
+ }
2361
+ }
2362
+
2363
+ {
2364
+ const langId = params.frLangId;
2365
+ const [existingPage] = await params.sql`
2366
+ SELECT id
2367
+ FROM public.pages
2368
+ WHERE language_id = ${langId} AND slug = 'boutique'
2369
+ `;
2370
+ let pageId = existingPage?.id as number | undefined;
2371
+
2372
+ if (!pageId) {
2373
+ const [newPage] = await params.sql`
2374
+ INSERT INTO public.pages (
2375
+ language_id,
2376
+ title,
2377
+ slug,
2378
+ status,
2379
+ meta_title,
2380
+ meta_description,
2381
+ translation_group_id
2382
+ )
2383
+ VALUES (
2384
+ ${langId},
2385
+ 'Boutique en Ligne',
2386
+ 'boutique',
2387
+ 'published',
2388
+ 'Boutique NextBlock™',
2389
+ 'Decouvrez nos produits premium',
2390
+ ${globalShopGroupId ?? null}
2391
+ )
2392
+ RETURNING id
2393
+ `;
2394
+ pageId = newPage.id as number;
2395
+
2396
+ const heroContent = {
2397
+ is_hero: true,
2398
+ container_type: 'full-width',
2399
+ background: {
2400
+ type: 'theme',
2401
+ theme: 'primary',
2402
+ },
2403
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
2404
+ column_gap: 'lg',
2405
+ padding: { top: 'xl', bottom: 'xl' },
2406
+ vertical_alignment: 'center',
2407
+ column_blocks: [
2408
+ [
2409
+ {
2410
+ block_type: 'heading',
2411
+ content: {
2412
+ level: 1,
2413
+ text_content: 'Boutique NextBlock™',
2414
+ textAlign: 'center',
2415
+ textColor: 'background',
2416
+ },
2417
+ },
2418
+ {
2419
+ block_type: 'text',
2420
+ content: {
2421
+ html_content:
2422
+ '<p style="text-align: center; color: var(--background); opacity: 0.9">Decouvrez notre selection premium d outils de developpement.</p>',
2423
+ },
2424
+ },
2425
+ ],
2426
+ ],
2427
+ };
2428
+
2429
+ const sectionContent = {
2430
+ container_type: 'container',
2431
+ background: { type: 'none' },
2432
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
2433
+ column_gap: 'none',
2434
+ padding: { top: 'xl', bottom: 'xl' },
2435
+ column_blocks: [
2436
+ [
2437
+ {
2438
+ block_type: 'heading',
2439
+ content: {
2440
+ level: 2,
2441
+ text_content: 'Produits Vedettes',
2442
+ textAlign: 'center',
2443
+ },
2444
+ },
2445
+ {
2446
+ block_type: 'product_grid',
2447
+ content: {
2448
+ type: 'latest',
2449
+ limit: 6,
2450
+ },
2451
+ },
2452
+ ],
2453
+ ],
2454
+ };
2455
+
2456
+ await params.sql`
2457
+ INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
2458
+ VALUES
2459
+ (${pageId}, ${langId}, 'section', ${params.sql.json(heroContent as any)}, 0),
2460
+ (${pageId}, ${langId}, 'section', ${params.sql.json(sectionContent as any)}, 1)
2461
+ `;
2462
+ }
2463
+
2464
+ const [exists] = await params.sql`
2465
+ SELECT id
2466
+ FROM public.navigation_items
2467
+ WHERE language_id = ${langId} AND url = '/boutique'
2468
+ `;
2469
+ if (!exists) {
2470
+ await params.sql`
2471
+ INSERT INTO public.navigation_items (language_id, menu_key, label, url, "order")
2472
+ VALUES (${langId}, 'HEADER', 'Boutique', '/boutique', 2)
2473
+ `;
2474
+ }
2475
+ }
2476
+
2477
+ console.log('[Sandbox Reset] Successfully created Shop pages and navigation.');
2478
+ }
2479
+
2480
+ async function seedCategoriesAndMappings(sql: SqlClient) {
2481
+ console.log('[Sandbox Reset] Seeding categories and product mappings...');
2482
+
2483
+ const categoriesToSeed = [
2484
+ {
2485
+ name: 'Software',
2486
+ slug: 'software',
2487
+ description: 'Developer software products and licenses.',
2488
+ name_translations: { fr: 'Logiciel' },
2489
+ description_translations: { fr: 'Logiciels et licences de développement.' }
2490
+ },
2491
+ {
2492
+ name: 'AI',
2493
+ slug: 'ai',
2494
+ description: 'Artificial Intelligence tools and neural components.',
2495
+ name_translations: { fr: 'IA' },
2496
+ description_translations: { fr: 'Outils d\'intelligence artificielle.' }
2497
+ },
2498
+ {
2499
+ name: 'Apparel',
2500
+ slug: 'apparel',
2501
+ description: 'Premium garments and studio uniforms built for developers.',
2502
+ name_translations: { fr: 'Vêtements' },
2503
+ description_translations: { fr: 'Vêtements de qualité et uniformes conçus pour les développeurs.' }
2504
+ },
2505
+ {
2506
+ name: 'Featured',
2507
+ slug: 'featured',
2508
+ description: 'Highlights and featured collection items.',
2509
+ name_translations: { fr: 'En vedette' },
2510
+ description_translations: { fr: 'Articles en vedette et nouveautés.' }
2511
+ },
2512
+ ];
2513
+
2514
+ const categoryMap = new Map<string, string>(); // slug -> id
2515
+
2516
+ for (const cat of categoriesToSeed) {
2517
+ const [inserted] = await sql`
2518
+ INSERT INTO public.categories (name, slug, description, name_translations, description_translations)
2519
+ VALUES (
2520
+ ${cat.name},
2521
+ ${cat.slug},
2522
+ ${cat.description},
2523
+ ${sql.json(cat.name_translations)},
2524
+ ${sql.json(cat.description_translations)}
2525
+ )
2526
+ ON CONFLICT (slug) DO UPDATE
2527
+ SET name = EXCLUDED.name,
2528
+ description = EXCLUDED.description,
2529
+ name_translations = EXCLUDED.name_translations,
2530
+ description_translations = EXCLUDED.description_translations
2531
+ RETURNING id
2532
+ `;
2533
+ if (inserted?.id) {
2534
+ categoryMap.set(cat.slug, inserted.id as string);
2535
+ }
2536
+ }
2537
+
2538
+ // Fetch all seeded products across both languages to ensure all translations are mapped.
2539
+ const products = await sql`
2540
+ SELECT id, sku, freemius_product_id
2541
+ FROM public.products
2542
+ WHERE freemius_product_id IN ('24851', '28609')
2543
+ OR sku IN ('NB-STUDIO-TEE', 'NB-SIGNAL-CAP', 'NB-UTILITY-PANTS')
2544
+ `;
2545
+
2546
+ const productIds = products.map((p) => p.id);
2547
+ if (productIds.length > 0) {
2548
+ await sql`
2549
+ DELETE FROM public.product_categories
2550
+ WHERE product_id = ANY(${productIds})
2551
+ `;
2552
+ }
2553
+
2554
+ const mappings: Array<{ product_id: string; category_id: string }> = [];
2555
+
2556
+ for (const prod of products) {
2557
+ const slugs: string[] = [];
2558
+ if (prod.freemius_product_id === '24851') {
2559
+ slugs.push('software', 'featured');
2560
+ } else if (prod.freemius_product_id === '28609') {
2561
+ slugs.push('software', 'ai');
2562
+ } else if (prod.sku === 'NB-STUDIO-TEE') {
2563
+ slugs.push('apparel', 'featured');
2564
+ } else if (prod.sku === 'NB-SIGNAL-CAP') {
2565
+ slugs.push('apparel');
2566
+ } else if (prod.sku === 'NB-UTILITY-PANTS') {
2567
+ slugs.push('apparel');
2568
+ }
2569
+
2570
+ for (const slug of slugs) {
2571
+ const catId = categoryMap.get(slug);
2572
+ if (catId) {
2573
+ mappings.push({
2574
+ product_id: prod.id as string,
2575
+ category_id: catId,
2576
+ });
2577
+ }
2578
+ }
2579
+ }
2580
+
2581
+ if (mappings.length > 0) {
2582
+ for (const mapping of mappings) {
2583
+ await sql`
2584
+ INSERT INTO public.product_categories (product_id, category_id)
2585
+ VALUES (${mapping.product_id}, ${mapping.category_id})
2586
+ ON CONFLICT DO NOTHING
2587
+ `;
2588
+ }
2589
+ console.log(`[Sandbox Reset] Seeded ${mappings.length} product-category associations.`);
2590
+ }
2591
+
2592
+ console.log('[Sandbox Reset] Successfully completed categories seeding.');
2593
+ }
2594
+
2595
+ async function seedFakeStoreData(sql: SqlClient, supabaseAdmin: any) {
2596
+ console.log('[Sandbox Reset] Starting fake store data seeding...');
2597
+
2598
+ // 1. Ensure Demo User
2599
+ const email = 'demo@nextblock.ca';
2600
+ console.log(`[Sandbox Reset] Checking for demo user: ${email}`);
2601
+ const { data: userData, error: userError } = await supabaseAdmin.auth.admin.listUsers();
2602
+ if (userError) {
2603
+ console.error('[Sandbox Reset] Auth listUsers error:', userError);
2604
+ throw userError;
2605
+ }
2606
+
2607
+ let demoUser = userData.users.find((u: any) => u.email === email);
2608
+ if (!demoUser) {
2609
+ console.log('[Sandbox Reset] Demo user missing in Auth, creating...');
2610
+ const { data: newUser, error: createError } = await supabaseAdmin.auth.admin.createUser({
2611
+ email: email,
2612
+ password: 'password',
2613
+ email_confirm: true,
2614
+ user_metadata: { full_name: 'Nextblock CMS' }
2615
+ });
2616
+ if (createError) {
2617
+ console.error('[Sandbox Reset] Auth createUser error:', createError);
2618
+ throw createError;
2619
+ }
2620
+ demoUser = newUser.user;
2621
+ console.log(`[Sandbox Reset] Created new demo user with ID: ${demoUser.id}`);
2622
+ } else {
2623
+ console.log(`[Sandbox Reset] Found existing demo user with ID: ${demoUser.id}`);
2624
+ }
2625
+
2626
+ const userId = demoUser.id;
2627
+
2628
+ // 2. Seed Invoice Branding
2629
+ console.log('[Sandbox Reset] Seeding invoice branding...');
2630
+ const branding = {
2631
+ business_name: 'NextBlock CMS',
2632
+ email: 'billing@nextblock.ca',
2633
+ phone: '5143188025',
2634
+ address: {
2635
+ line1: '',
2636
+ line2: '',
2637
+ city: 'Salaberry-de-Valleyfield',
2638
+ state: 'Quebec',
2639
+ postal_code: 'J6S 5B6',
2640
+ country_code: 'CA',
2641
+ },
2642
+ tax_registrations: [],
2643
+ };
2644
+
2645
+ await sql`
2646
+ INSERT INTO public.site_settings (key, value)
2647
+ VALUES ('invoice_settings', ${sql.json(branding)})
2648
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
2649
+ `;
2650
+
2651
+ // 3. Seed Profile
2652
+ console.log('[Sandbox Reset] Seeding demo account profile (ADMIN)...');
2653
+ await sql`
2654
+ INSERT INTO public.profiles (id, full_name, website, role, updated_at)
2655
+ VALUES (${userId}, 'Nextblock CMS', 'https://nextblock.dev', 'ADMIN', now())
2656
+ ON CONFLICT (id) DO UPDATE SET
2657
+ full_name = EXCLUDED.full_name,
2658
+ website = EXCLUDED.website,
2659
+ role = 'ADMIN',
2660
+ updated_at = now()
2661
+ `;
2662
+
2663
+ // 4. Seed Orders
2664
+ console.log('[Sandbox Reset] Querying products for order seeding...');
2665
+ const products = await sql`
2666
+ SELECT id, price, title
2667
+ FROM public.products
2668
+ WHERE status IN ('active', 'published')
2669
+ LIMIT 10
2670
+ `;
2671
+
2672
+ console.log(`[Sandbox Reset] Found ${products.length} products for orders.`);
2673
+
2674
+ if (products.length > 0) {
2675
+ console.log(`[Sandbox Reset] Cleaning up existing orders for user ${userId}...`);
2676
+ await sql`DELETE FROM public.orders WHERE user_id = ${userId}`;
2677
+
2678
+ console.log('[Sandbox Reset] Inserting 5 fake orders...');
2679
+ for (let i = 0; i < 5; i++) {
2680
+ try {
2681
+ const product = products[i % products.length];
2682
+ const quantity = Math.floor(Math.random() * 2) + 1;
2683
+ const total = (product.price || 0) * quantity;
2684
+ const orderId = crypto.randomUUID();
2685
+ const invoiceNumber = `INV-2024-${1000 + i}`;
2686
+ const hoursAgo = `${i * 2} hours`;
2687
+
2688
+ console.log(`[Sandbox Reset] Creating order ${i+1}/5: ${invoiceNumber} for product ${product.title}`);
2689
+
2690
+ await sql`
2691
+ INSERT INTO public.orders (
2692
+ id, user_id, status, total, subtotal, tax_total, currency,
2693
+ invoice_number, paid_at, created_at, customer_details, provider
2694
+ ) VALUES (
2695
+ ${orderId}, ${userId}, 'paid', ${total}, ${total}, 0, 'USD',
2696
+ ${invoiceNumber}, now() - ${hoursAgo}::interval, now() - ${hoursAgo}::interval,
2697
+ ${sql.json({ email, name: 'Nextblock CMS' })}, 'stripe'
2698
+ )
2699
+ `;
2700
+
2701
+ await sql`
2702
+ INSERT INTO public.order_items (order_id, product_id, quantity, price_at_purchase)
2703
+ VALUES (${orderId}, ${product.id}, ${quantity}, ${product.price})
2704
+ `;
2705
+ } catch (orderErr: any) {
2706
+ console.error(`[Sandbox Reset] Failed to insert order ${i}:`, orderErr.message || orderErr);
2707
+ }
2708
+ }
2709
+ console.log('[Sandbox Reset] Finished order seeding loop.');
2710
+ } else {
2711
+ console.warn('[Sandbox Reset] Skipping order seeding: No products found.');
2712
+ }
2713
+ }
5
2714
 
6
2715
  export async function GET(request: NextRequest) {
7
- // 1. Guard: Only run in Sandbox Mode
8
- if (process.env.NEXT_PUBLIC_IS_SANDBOX !== 'true') {
9
- return NextResponse.json({ message: 'Sandbox reset skipped: Not in Sandbox Mode' });
2716
+ // 1. Guard: fail closed anywhere that is not explicitly the sandbox.
2717
+ const isSandboxResetEnabled = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true';
2718
+
2719
+ if (!isSandboxResetEnabled) {
2720
+ return new NextResponse('Not Found', { status: 404 });
10
2721
  }
11
2722
 
12
2723
  // 2. Guard: Verify Cron Secret
@@ -20,28 +2731,713 @@ export async function GET(request: NextRequest) {
20
2731
  const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
21
2732
  const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
22
2733
 
23
- if (!supabaseUrl || !supabaseServiceKey) {
24
- return NextResponse.json({ error: 'Missing Supabase environment variables' }, { status: 500 });
2734
+ const r2AccountId = process.env.R2_ACCOUNT_ID;
2735
+ const r2AccessKeyId = process.env.R2_ACCESS_KEY_ID;
2736
+ const r2SecretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
2737
+ const r2BucketName = process.env.R2_BUCKET_NAME;
2738
+ let siteUrl = process.env.NEXT_PUBLIC_URL || request.nextUrl.origin;
2739
+
2740
+ if (siteUrl && siteUrl.includes('localhost:') && request.nextUrl.origin.includes('localhost:')) {
2741
+ siteUrl = request.nextUrl.origin;
2742
+ }
2743
+ if (siteUrl && !siteUrl.startsWith('http')) {
2744
+ siteUrl = `https://${siteUrl}`;
2745
+ }
2746
+ if (siteUrl && siteUrl.endsWith('/')) {
2747
+ siteUrl = siteUrl.slice(0, -1);
2748
+ }
2749
+
2750
+ if (
2751
+ !supabaseUrl ||
2752
+ !supabaseServiceKey ||
2753
+ !r2AccountId ||
2754
+ !r2AccessKeyId ||
2755
+ !r2SecretAccessKey ||
2756
+ !r2BucketName ||
2757
+ !siteUrl
2758
+ ) {
2759
+ return NextResponse.json({ error: 'Missing environment variables' }, { status: 500 });
25
2760
  }
26
2761
 
27
- const supabase = createClient(supabaseUrl, supabaseServiceKey, {
28
- auth: {
29
- autoRefreshToken: false,
30
- persistSession: false,
2762
+ const s3 = new S3Client({
2763
+ region: 'auto',
2764
+ endpoint: `https://${r2AccountId}.r2.cloudflarestorage.com`,
2765
+ credentials: {
2766
+ accessKeyId: r2AccessKeyId,
2767
+ secretAccessKey: r2SecretAccessKey,
31
2768
  },
32
2769
  });
33
2770
 
34
2771
  try {
35
- const { error } = await supabase.rpc('reset_sandbox');
2772
+ console.log('[Sandbox Reset] Starting Hard Reset...');
2773
+
2774
+ console.log('[Sandbox Reset] Wiping R2 Bucket...');
2775
+ let continuationToken: string | undefined;
2776
+ do {
2777
+ const listCmd = new ListObjectsV2Command({
2778
+ Bucket: r2BucketName,
2779
+ ContinuationToken: continuationToken,
2780
+ });
2781
+ const listRes = await s3.send(listCmd);
2782
+
2783
+ if (listRes.Contents && listRes.Contents.length > 0) {
2784
+ const objectsToDelete = listRes.Contents.map((obj) => ({ Key: obj.Key }));
2785
+ await s3.send(
2786
+ new DeleteObjectsCommand({
2787
+ Bucket: r2BucketName,
2788
+ Delete: { Objects: objectsToDelete },
2789
+ })
2790
+ );
2791
+ console.log(`[Sandbox Reset] Deleted ${objectsToDelete.length} objects.`);
2792
+ }
2793
+
2794
+ continuationToken = listRes.NextContinuationToken;
2795
+ } while (continuationToken);
2796
+
2797
+ console.log('[Sandbox Reset] Fetching and re-seeding assets...');
2798
+ const uploadedAssets = await uploadSeedAssets({
2799
+ s3,
2800
+ bucketName: r2BucketName,
2801
+ siteUrl,
2802
+ });
2803
+
2804
+ console.log('[Sandbox Reset] Resetting Database...');
2805
+ const dbUrl = process.env.POSTGRES_URL || process.env.DATABASE_URL;
2806
+ if (!dbUrl) {
2807
+ throw new Error('Missing POSTGRES_URL environment variable');
2808
+ }
2809
+
2810
+ const db = postgres(dbUrl, { ssl: 'require', onnotice: () => undefined });
2811
+ try {
2812
+ try {
2813
+ await db.unsafe(SANDBOX_RESET_SQL);
2814
+ console.log('[Sandbox Reset] Database re-seeded successfully.');
2815
+ } catch (dbError: any) {
2816
+ console.error('[Sandbox Reset] DB Error:', dbError);
2817
+ throw dbError;
2818
+ }
2819
+
2820
+ const normalizedMediaCount = await normalizeMediaStorageKeys(db);
2821
+ if (normalizedMediaCount > 0) {
2822
+ console.log(
2823
+ `[Sandbox Reset] Normalized ${normalizedMediaCount} media storage key(s) after SQL reset.`
2824
+ );
2825
+ }
2826
+
2827
+ await ensureCoreMediaRecords({
2828
+ sql: db,
2829
+ uploadedAssets,
2830
+ });
2831
+
2832
+ console.log('[Sandbox Reset] Pre-activating premium packages...');
2833
+ const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
2834
+ auth: {
2835
+ autoRefreshToken: false,
2836
+ persistSession: false,
2837
+ },
2838
+ });
2839
+
2840
+ if (process.env.FREEMIUS_ECOMMERCE_SANDBOX_KEY) {
2841
+ const { error: activationError } = await supabaseAdmin.from('package_activations').insert({
2842
+ package_id: 'ecommerce',
2843
+ license_key: process.env.FREEMIUS_ECOMMERCE_SANDBOX_KEY,
2844
+ status: 'active',
2845
+ instance_name: siteUrl,
2846
+ });
2847
+
2848
+ if (activationError) {
2849
+ console.error('[Sandbox Reset] Failed to activate ecommerce package:', activationError.message);
2850
+ throw activationError;
2851
+ } else {
2852
+ console.log('[Sandbox Reset] Successfully activated ecommerce package.');
2853
+
2854
+ // Dynamically populate the store with Freemius products
2855
+ try {
2856
+ console.log('[Sandbox Reset] Syncing products from Freemius...');
2857
+ const syncRes = await syncFreemiusProductsToSupabase();
2858
+ console.log(`[Sandbox Reset] Synced ${syncRes?.count || 0} products.`);
2859
+ await db`
2860
+ INSERT INTO public.site_settings (key, value)
2861
+ VALUES (
2862
+ 'enabled_payment_providers',
2863
+ '{"stripe": true, "freemius": true}'::jsonb
2864
+ )
2865
+ ON CONFLICT (key) DO UPDATE
2866
+ SET value = EXCLUDED.value
2867
+ `;
2868
+
2869
+ try {
2870
+ const { enLangId, frLangId } = await getLanguageIds(db);
2871
+ await ensureSandboxCommerceProductSynced({
2872
+ sql: db,
2873
+ enLangId,
2874
+ });
2875
+ const commerceAsset = uploadedAssets.get('images/commerce-square.webp');
2876
+
2877
+ if (!commerceAsset) {
2878
+ throw new Error('Missing uploaded Commerce Pro asset after R2 seed step.');
2879
+ }
2880
+
2881
+ await enrichCommerceProducts({
2882
+ sql: db,
2883
+ commerceAsset,
2884
+ enLangId,
2885
+ frLangId,
2886
+ });
2887
+
2888
+ const cortexAsset = uploadedAssets.get('images/cortex-ai-square.webp');
2889
+ if (!cortexAsset) {
2890
+ throw new Error('Missing uploaded Cortex AI asset after R2 seed step.');
2891
+ }
2892
+
2893
+ await enrichCortexAiProducts({
2894
+ sql: db,
2895
+ cortexAsset,
2896
+ enLangId,
2897
+ frLangId,
2898
+ });
2899
+
2900
+ await db.begin(async (sql: any) => {
2901
+ const tx = sql as SqlClient;
2902
+
2903
+ await seedApparelCatalog({
2904
+ sql: tx,
2905
+ enLangId,
2906
+ frLangId,
2907
+ uploadedAssets,
2908
+ });
2909
+ });
2910
+
2911
+ await ensureShopPagesAndNavigation({
2912
+ sql: db,
2913
+ enLangId,
2914
+ frLangId,
2915
+ });
2916
+
2917
+ await seedCategoriesAndMappings(db);
2918
+ } catch (enrichErr: any) {
2919
+ console.error('[Sandbox Reset] Product enrichment failed:', enrichErr.message || enrichErr);
2920
+ throw enrichErr;
2921
+ }
2922
+
2923
+ /*
2924
+ // Post-sync enrichment: Add image and rich description to the Commerce Pro product
2925
+ try {
2926
+ console.log('[Sandbox Reset] Enriching NextBlock™ Commerce Pro...');
2927
+ const commerceLogoKey = 'images/commerce-square.webp';
2928
+
2929
+ // 0. Get language IDs
2930
+ const [enLangRaw] = await db`SELECT id FROM public.languages WHERE code = 'en' LIMIT 1`;
2931
+ const [frLangRaw] = await db`SELECT id FROM public.languages WHERE code = 'fr' LIMIT 1`;
2932
+ const enLangId = enLangRaw?.id;
2933
+ const frLangId = frLangRaw?.id;
2934
+
2935
+ // 1. Ensure media record exists for the seeded asset
2936
+ const [mediaRecord] = await db`
2937
+ INSERT INTO public.media (file_name, object_key, file_path, file_type, size_bytes)
2938
+ VALUES ('commerce-square.webp', ${commerceLogoKey}, ${commerceLogoKey}, 'image/webp', 1651652)
2939
+ ON CONFLICT (object_key) DO UPDATE SET file_path = EXCLUDED.file_path
2940
+ RETURNING id
2941
+ `;
2942
+
2943
+ // 2. Find the synced product (NextBlock™ Commerce Pro)
2944
+ const [product] = await db`
2945
+ SELECT * FROM public.products
2946
+ WHERE freemius_product_id = '24851' AND language_id = ${enLangId}
2947
+ LIMIT 1
2948
+ `;
2949
+
2950
+ if (product && mediaRecord) {
2951
+ // 3. Link media to English product
2952
+ await db`
2953
+ INSERT INTO public.product_media (product_id, media_id, sort_order)
2954
+ VALUES (${product.id}, ${mediaRecord.id}, 0)
2955
+ ON CONFLICT (product_id, media_id) DO NOTHING
2956
+ `;
2957
+
2958
+ // 4. Update English descriptions
2959
+ const shortDescEn = "NextBlock™ Ecommerce is an AI-native, block-based storefront engine for Next.js. Featuring a premium, developer-first aesthetic and high-performance edge rendering.";
2960
+
2961
+ const htmlDescriptionEn = {
2962
+ type: "doc",
2963
+ content: [
2964
+ {
2965
+ type: "heading",
2966
+ attrs: { level: 2 },
2967
+ content: [{ type: "text", text: "🚀 The Future of Digital Commerce" }]
2968
+ },
2969
+ {
2970
+ type: "paragraph",
2971
+ content: [
2972
+ {
2973
+ type: "text",
2974
+ text: "NextBlock™ Ecommerce bridges the gap between high-performance headless architecture and intuitive visual editing. Built on the NextBlock™ Performance Stack (NPS), it leverages Next.js 16, Supabase, and Tailwind CSS to deliver sub-millisecond latency and a seamless \"Vibe Coding\" experience."
2975
+ }
2976
+ ]
2977
+ },
2978
+ {
2979
+ type: "heading",
2980
+ attrs: { level: 3 },
2981
+ content: [{ type: "text", text: "🎨 Notion-Style Editor" }]
2982
+ },
2983
+ {
2984
+ type: "paragraph",
2985
+ content: [
2986
+ {
2987
+ type: "text",
2988
+ text: "Stop fighting with complex backends. Our Tiptap-powered editor provides a familiar, block-based interface that allows you to build stunning product pages as easily as writing a document."
2989
+ }
2990
+ ]
2991
+ },
2992
+ {
2993
+ type: "heading",
2994
+ attrs: { level: 3 },
2995
+ content: [{ type: "text", text: "🛡️ Secure by Design" }]
2996
+ },
2997
+ {
2998
+ type: "paragraph",
2999
+ content: [
3000
+ {
3001
+ type: "text",
3002
+ text: "Integrated with Freemius for cryptographic licensing and recurring billing. Features dual-layer payment strategy with Freemius MoR and native Stripe support."
3003
+ }
3004
+ ]
3005
+ },
3006
+ {
3007
+ type: "heading",
3008
+ attrs: { level: 3 },
3009
+ content: [{ type: "text", text: "Key Technical Specs" }]
3010
+ },
3011
+ {
3012
+ type: "bulletList",
3013
+ content: [
3014
+ {
3015
+ type: "listItem",
3016
+ content: [
3017
+ {
3018
+ type: "paragraph",
3019
+ content: [{ type: "text", text: "⚡ ISR & Edge Caching: Sub-millisecond Time to First Byte (TTFB) globally." }]
3020
+ }
3021
+ ]
3022
+ },
3023
+ {
3024
+ type: "listItem",
3025
+ content: [
3026
+ {
3027
+ type: "paragraph",
3028
+ content: [{ type: "text", text: "📦 Nx Monorepo: Strictly decoupled architecture for ultimate scalability and code-splitting." }]
3029
+ }
3030
+ ]
3031
+ },
3032
+ {
3033
+ type: "listItem",
3034
+ content: [
3035
+ {
3036
+ type: "paragraph",
3037
+ content: [{ type: "text", text: "🖼️ AVIF Optimization: 20% smaller media payloads with native Next.js Image component integration." }]
3038
+ }
3039
+ ]
3040
+ }
3041
+ ]
3042
+ },
3043
+ {
3044
+ type: "heading",
3045
+ attrs: { level: 3 },
3046
+ content: [{ type: "text", text: "Ready for the \"Vibe Coding\" Era" }]
3047
+ },
3048
+ {
3049
+ type: "paragraph",
3050
+ content: [
3051
+ {
3052
+ type: "text",
3053
+ text: "NextBlock™ is built from the ground up to be extendable by AI Agents. Whether you're using Claude, v0, or custom GPTs, our highly typed Block SDK and Zod schema validations ensure every extension stays robust and secure."
3054
+ }
3055
+ ]
3056
+ }
3057
+ ]
3058
+ };
3059
+
3060
+ await db`
3061
+ UPDATE public.products
3062
+ SET short_description = ${shortDescEn},
3063
+ description_json = ${db.json(htmlDescriptionEn)},
3064
+ product_type = 'digital',
3065
+ payment_provider = 'freemius'
3066
+ WHERE id = ${product.id}
3067
+ `;
3068
+
3069
+ // 5. Create French Version
3070
+ if (frLangId) {
3071
+ console.log('[Sandbox Reset] Creating French version of NextBlock™ Commerce Pro...');
3072
+
3073
+ const shortDescFr = "NextBlock™ Ecommerce est un moteur de boutique basé sur des blocs et natif de l'IA pour Next.js. Doté d'une esthétique premium et d'un rendu edge haute performance.";
3074
+
3075
+ const htmlDescriptionFr = {
3076
+ type: "doc",
3077
+ content: [
3078
+ {
3079
+ type: "heading",
3080
+ attrs: { level: 2 },
3081
+ content: [{ type: "text", text: "🚀 Le futur du commerce numérique" }]
3082
+ },
3083
+ {
3084
+ type: "paragraph",
3085
+ content: [
3086
+ {
3087
+ type: "text",
3088
+ text: "NextBlock™ Ecommerce comble le fossé entre l'architecture headless haute performance et l'édition visuelle intuitive. Construit sur la NextBlock™ Performance Stack (NPS), il exploite Next.js 16, Supabase et Tailwind CSS pour offrir une latence de moins d'une milliseconde."
3089
+ }
3090
+ ]
3091
+ },
3092
+ {
3093
+ type: "heading",
3094
+ attrs: { level: 3 },
3095
+ content: [{ type: "text", text: "🎨 Éditeur style Notion" }]
3096
+ },
3097
+ {
3098
+ type: "paragraph",
3099
+ content: [
3100
+ {
3101
+ type: "text",
3102
+ text: "Arrêtez de vous battre avec des backends complexes. Notre éditeur propulsé par Tiptap offre une interface familière basée sur des blocs qui vous permet de créer de superbes pages produits aussi facilement qu'un document."
3103
+ }
3104
+ ]
3105
+ },
3106
+ {
3107
+ type: "heading",
3108
+ attrs: { level: 3 },
3109
+ content: [{ type: "text", text: "🛡️ Sécurisé par conception" }]
3110
+ },
3111
+ {
3112
+ type: "paragraph",
3113
+ content: [
3114
+ {
3115
+ type: "text",
3116
+ text: "Intégré avec Freemius pour les licences cryptographiques et la facturation récurrente. Stratégie de paiement à double couche avec Freemius MoR et support natif Stripe."
3117
+ }
3118
+ ]
3119
+ },
3120
+ {
3121
+ type: "heading",
3122
+ attrs: { level: 3 },
3123
+ content: [{ type: "text", text: "Spécifications techniques clés" }]
3124
+ },
3125
+ {
3126
+ type: "bulletList",
3127
+ content: [
3128
+ {
3129
+ type: "listItem",
3130
+ content: [
3131
+ {
3132
+ type: "paragraph",
3133
+ content: [{ type: "text", text: "⚡ ISR & Mise en cache Edge : Temps de premier octet (TTFB) inférieur à la milliseconde." }]
3134
+ }
3135
+ ]
3136
+ },
3137
+ {
3138
+ type: "listItem",
3139
+ content: [
3140
+ {
3141
+ type: "paragraph",
3142
+ content: [{ type: "text", text: "📦 Monorepo Nx : Architecture strictement découplée pour une évolutivité ultime." }]
3143
+ }
3144
+ ]
3145
+ },
3146
+ {
3147
+ type: "listItem",
3148
+ content: [
3149
+ {
3150
+ type: "paragraph",
3151
+ content: [{ type: "text", text: "🖼️ Optimisation AVIF : Payloads média 20 % plus petits avec Next.js Image." }]
3152
+ }
3153
+ ]
3154
+ }
3155
+ ]
3156
+ }
3157
+ ]
3158
+ };
3159
+
3160
+ const [frProduct] = await db`
3161
+ INSERT INTO public.products (
3162
+ sku, title, slug, price, sale_price, stock, status,
3163
+ short_description, description_json,
3164
+ product_type, payment_provider,
3165
+ language_id, translation_group_id,
3166
+ freemius_product_id, freemius_plan_id,
3167
+ trial_period_days, trial_requires_payment_method
3168
+ )
3169
+ VALUES (
3170
+ ${product.sku}, 'NextBlock™ Commerce Pro - Licence Commerce', ${product.slug + '-fr'},
3171
+ ${product.price}, ${product.sale_price}, ${product.stock || 99}, ${product.status},
3172
+ ${shortDescFr}, ${db.json(htmlDescriptionFr)},
3173
+ 'digital', 'freemius',
3174
+ ${frLangId}, ${product.translation_group_id},
3175
+ ${product.freemius_product_id}, ${product.freemius_plan_id},
3176
+ ${product.trial_period_days ?? 0}, ${product.trial_requires_payment_method ?? false}
3177
+ )
3178
+ ON CONFLICT ON CONSTRAINT products_language_id_slug_key DO UPDATE
3179
+ SET
3180
+ title = EXCLUDED.title,
3181
+ short_description = EXCLUDED.short_description,
3182
+ description_json = EXCLUDED.description_json,
3183
+ product_type = EXCLUDED.product_type,
3184
+ payment_provider = EXCLUDED.payment_provider,
3185
+ trial_period_days = EXCLUDED.trial_period_days,
3186
+ trial_requires_payment_method = EXCLUDED.trial_requires_payment_method
3187
+ RETURNING id
3188
+ `;
3189
+
3190
+ if (frProduct) {
3191
+ await db`
3192
+ INSERT INTO public.product_media (product_id, media_id, sort_order)
3193
+ VALUES (${frProduct.id}, ${mediaRecord.id}, 0)
3194
+ ON CONFLICT (product_id, media_id) DO NOTHING
3195
+ `;
3196
+ }
3197
+ }
3198
+ console.log('[Sandbox Reset] Successfully enriched commerce products (EN & FR).');
3199
+ }
3200
+
3201
+
3202
+ // 6. Add Shop Pages & Navigation Items
3203
+ console.log('[Sandbox Reset] Adding Shop Pages and navigation items...');
3204
+ let globalShopGroupId: string | undefined;
3205
+
3206
+ if (enLangId) {
3207
+ const langId = enLangId;
3208
+
3209
+ // Insert Page
3210
+ const [existingPage] = await db`SELECT id, translation_group_id FROM public.pages WHERE language_id = ${langId} AND slug = 'shop'`;
3211
+ let pageId = existingPage?.id;
3212
+ globalShopGroupId = existingPage?.translation_group_id;
3213
+
3214
+ if (!pageId) {
3215
+ const [newPage] = await db`
3216
+ INSERT INTO public.pages (language_id, title, slug, status, meta_title, meta_description)
3217
+ VALUES (${langId}, 'Shop Our Products', 'shop', 'published', 'NextBlock™ Store', 'Browse our premium products')
3218
+ RETURNING id, translation_group_id
3219
+ `;
3220
+ pageId = newPage.id;
3221
+ globalShopGroupId = newPage?.translation_group_id;
3222
+
3223
+ const heroContent = {
3224
+ is_hero: true,
3225
+ container_type: "full-width",
3226
+ background: {
3227
+ type: "theme",
3228
+ theme: "primary"
3229
+ },
3230
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
3231
+ column_gap: "lg",
3232
+ padding: { top: "xl", bottom: "xl" },
3233
+ vertical_alignment: "center",
3234
+ column_blocks: [
3235
+ [
3236
+ {
3237
+ block_type: "heading",
3238
+ content: {
3239
+ level: 1,
3240
+ text_content: "NextBlock™ Store",
3241
+ textAlign: "center",
3242
+ textColor: "background"
3243
+ }
3244
+ },
3245
+ {
3246
+ block_type: "text",
3247
+ content: {
3248
+ html_content: "<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Discover our premium selection of developer tools and digital commerce solutions.</p>"
3249
+ }
3250
+ }
3251
+ ]
3252
+ ]
3253
+ };
3254
+
3255
+ const sectionContent = {
3256
+ container_type: "container",
3257
+ background: { type: "none" },
3258
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
3259
+ column_gap: "none",
3260
+ padding: { top: "xl", bottom: "xl" },
3261
+ column_blocks: [
3262
+ [
3263
+ {
3264
+ block_type: "heading",
3265
+ content: {
3266
+ level: 2,
3267
+ text_content: "Featured Products",
3268
+ textAlign: "center"
3269
+ }
3270
+ },
3271
+ {
3272
+ block_type: "product_grid",
3273
+ content: {
3274
+ type: "latest",
3275
+ limit: 6
3276
+ }
3277
+ }
3278
+ ]
3279
+ ]
3280
+ };
3281
+
3282
+ await db`
3283
+ INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
3284
+ VALUES
3285
+ (${pageId}, ${langId}, 'section', ${db.json(heroContent as any)}, 0),
3286
+ (${pageId}, ${langId}, 'section', ${db.json(sectionContent as any)}, 1)
3287
+ `;
3288
+ }
3289
+
3290
+ const [exists] = await db`SELECT id FROM public.navigation_items WHERE language_id = ${langId} AND url = '/shop'`;
3291
+ if (!exists) {
3292
+ await db`
3293
+ INSERT INTO public.navigation_items (language_id, menu_key, label, url, "order")
3294
+ VALUES (${langId}, 'HEADER', 'Shop', '/shop', 2)
3295
+ `;
3296
+ }
3297
+ }
3298
+
3299
+ if (frLangId) {
3300
+ const langId = frLangId;
3301
+
3302
+ // Insert French Page (keep slug 'boutique' matching original nav link)
3303
+ const [existingPage] = await db`SELECT id FROM public.pages WHERE language_id = ${langId} AND slug = 'boutique'`;
3304
+ let pageId = existingPage?.id;
3305
+
3306
+ if (!pageId) {
3307
+ const [newPage] = await db`
3308
+ INSERT INTO public.pages (language_id, title, slug, status, meta_title, meta_description, translation_group_id)
3309
+ VALUES (${langId}, 'Boutique en Ligne', 'boutique', 'published', 'Boutique NextBlock™', 'Découvrez nos produits premium', ${globalShopGroupId ?? null})
3310
+ RETURNING id
3311
+ `;
3312
+ pageId = newPage.id;
3313
+
3314
+ const heroContent = {
3315
+ is_hero: true,
3316
+ container_type: "full-width",
3317
+ background: {
3318
+ type: "theme",
3319
+ theme: "primary"
3320
+ },
3321
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
3322
+ column_gap: "lg",
3323
+ padding: { top: "xl", bottom: "xl" },
3324
+ vertical_alignment: "center",
3325
+ column_blocks: [
3326
+ [
3327
+ {
3328
+ block_type: "heading",
3329
+ content: {
3330
+ level: 1,
3331
+ text_content: "Boutique NextBlock™",
3332
+ textAlign: "center",
3333
+ textColor: "background"
3334
+ }
3335
+ },
3336
+ {
3337
+ block_type: "text",
3338
+ content: {
3339
+ html_content: "<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Découvrez notre sélection premium d'outils de développement.</p>"
3340
+ }
3341
+ }
3342
+ ]
3343
+ ]
3344
+ };
3345
+
3346
+ const sectionContent = {
3347
+ container_type: "container",
3348
+ background: { type: "none" },
3349
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 1 },
3350
+ column_gap: "none",
3351
+ padding: { top: "xl", bottom: "xl" },
3352
+ column_blocks: [
3353
+ [
3354
+ {
3355
+ block_type: "heading",
3356
+ content: {
3357
+ level: 2,
3358
+ text_content: "Produits Vedettes",
3359
+ textAlign: "center"
3360
+ }
3361
+ },
3362
+ {
3363
+ block_type: "product_grid",
3364
+ content: {
3365
+ type: "latest",
3366
+ limit: 6
3367
+ }
3368
+ }
3369
+ ]
3370
+ ]
3371
+ };
3372
+
3373
+ await db`
3374
+ INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
3375
+ VALUES
3376
+ (${pageId}, ${langId}, 'section', ${db.json(heroContent as any)}, 0),
3377
+ (${pageId}, ${langId}, 'section', ${db.json(sectionContent as any)}, 1)
3378
+ `;
3379
+ }
3380
+
3381
+ const [exists] = await db`SELECT id FROM public.navigation_items WHERE language_id = ${langId} AND url = '/boutique'`;
3382
+ if (!exists) {
3383
+ await db`
3384
+ INSERT INTO public.navigation_items (language_id, menu_key, label, url, "order")
3385
+ VALUES (${langId}, 'HEADER', 'Boutique', '/boutique', 2)
3386
+ `;
3387
+ }
3388
+ }
3389
+ console.log('[Sandbox Reset] Successfully created Shop pages and navigation.');
3390
+ } catch (enrichErr: any) {
3391
+ console.error('[Sandbox Reset] Product enrichment failed:', enrichErr.message || enrichErr);
3392
+ }
3393
+ */
3394
+ } catch (syncErr: any) {
3395
+ console.error('[Sandbox Reset] Failed to sync Freemius products:', syncErr.message || syncErr);
3396
+ throw syncErr;
3397
+ }
3398
+ }
3399
+ }
3400
+
3401
+ if (process.env.FREEMIUS_AI_SANDBOX_KEY) {
3402
+ const { error: cortexActivationError } = await supabaseAdmin
3403
+ .from('package_activations')
3404
+ .upsert(
3405
+ {
3406
+ package_id: CORTEX_AI_PACKAGE_ID,
3407
+ license_key: process.env.FREEMIUS_AI_SANDBOX_KEY,
3408
+ status: 'active',
3409
+ instance_name: siteUrl,
3410
+ last_validated_at: new Date().toISOString(),
3411
+ },
3412
+ { onConflict: 'license_key, package_id' }
3413
+ );
3414
+
3415
+ if (cortexActivationError) {
3416
+ console.error(
3417
+ '[Sandbox Reset] Failed to activate Cortex AI package:',
3418
+ cortexActivationError.message
3419
+ );
3420
+ throw cortexActivationError;
3421
+ } else {
3422
+ console.log('[Sandbox Reset] Successfully activated Cortex AI package.');
3423
+ }
3424
+ }
36
3425
 
37
- if (error) {
38
- console.error('Error resetting sandbox:', error);
39
- return NextResponse.json({ error: error.message }, { status: 500 });
3426
+ // Seed additional store data: Branding, Demo Account, and Fake Orders
3427
+ try {
3428
+ await seedFakeStoreData(db, supabaseAdmin);
3429
+ console.log('[Sandbox Reset] Successfully seeded fake store data.');
3430
+ } catch (storeSeedErr: any) {
3431
+ console.error('[Sandbox Reset] Failed to seed store data:', storeSeedErr.message || storeSeedErr);
3432
+ }
3433
+ } finally {
3434
+ await db.end();
40
3435
  }
41
3436
 
42
- return NextResponse.json({ success: true, message: 'Sandbox reset successfully' });
43
- } catch (err) {
44
- console.error('Unexpected error resetting sandbox:', err);
45
- return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
3437
+ console.log('[Sandbox Reset] Complete.');
3438
+ return NextResponse.json({ success: true, message: 'Sandbox hard reset completed successfully' });
3439
+ } catch (err: any) {
3440
+ console.error('[Sandbox Reset] Unexpected error:', err);
3441
+ return NextResponse.json({ error: err.message || 'Internal Server Error', stack: err.stack }, { status: 500 });
46
3442
  }
47
3443
  }