create-nextblock 0.2.78 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/bin/create-nextblock.js +793 -472
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,1004 @@
1
+ "use client";
2
+
3
+ import { useRef, useState, useTransition } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { toast } from "sonner";
6
+ import {
7
+ Alert,
8
+ AlertDescription,
9
+ AlertTitle,
10
+ Badge,
11
+ Button,
12
+ Card,
13
+ CardContent,
14
+ CardDescription,
15
+ CardHeader,
16
+ CardTitle,
17
+ Checkbox,
18
+ Input,
19
+ Label,
20
+ Progress,
21
+ RadioGroup,
22
+ RadioGroupItem,
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ Separator,
29
+ Spinner,
30
+ } from "@nextblock-cms/ui";
31
+ import { AlertTriangle, CheckCircle2, Download, Upload } from "lucide-react";
32
+
33
+ import type {
34
+ CmsContentType,
35
+ CmsImportApplyMode,
36
+ CmsImportConflictMode,
37
+ CmsImportSummary,
38
+ } from "../../../../lib/cms-transfer/types";
39
+ import {
40
+ applyCmsBackupBundleImportAction,
41
+ dryRunCmsBackupBundleImportAction,
42
+ exportCmsBackupBundleAction,
43
+ } from "../../import-export/actions";
44
+
45
+ type BackupMode = "content_json" | "full_zip";
46
+ type FullBackupMediaUrlMode = "rewrite_to_destination" | "keep_exact_urls";
47
+ type ProgressScope = "export" | "restore";
48
+
49
+ interface OperationProgress {
50
+ scope: ProgressScope;
51
+ label: string;
52
+ detail: string;
53
+ value: number;
54
+ cap: number;
55
+ }
56
+
57
+ interface FullBackupSummary {
58
+ success: boolean;
59
+ error?: string;
60
+ exportedAt?: string;
61
+ sourceBucket?: string | null;
62
+ sourceBaseUrl?: string | null;
63
+ databaseDumpBytes?: number;
64
+ contentBundleBytes?: number;
65
+ storageObjects?: number;
66
+ missingStorageObjects?: number;
67
+ warnings: string[];
68
+ databaseRestored?: boolean;
69
+ storageObjectsUploaded?: number;
70
+ storageObjectsDeleted?: number;
71
+ mediaUrlRewriteMode?: FullBackupMediaUrlMode;
72
+ mediaUrlRowsUpdated?: number;
73
+ }
74
+
75
+ const CONTENT_TYPES: Array<{ value: CmsContentType; label: string }> = [
76
+ { value: "pages", label: "Pages" },
77
+ { value: "posts", label: "Posts" },
78
+ { value: "products", label: "Products" },
79
+ ];
80
+
81
+ const FULL_RESTORE_CONFIRMATION = "RESTORE FULL BACKUP";
82
+
83
+ function downloadTextFile(fileName: string, mimeType: string, content: string) {
84
+ const blob = new Blob([content], { type: `${mimeType};charset=utf-8` });
85
+ downloadBlobFile(fileName, blob);
86
+ }
87
+
88
+ function downloadBlobFile(fileName: string, blob: Blob) {
89
+ const url = URL.createObjectURL(blob);
90
+ const link = document.createElement("a");
91
+ link.href = url;
92
+ link.download = fileName;
93
+ document.body.appendChild(link);
94
+ link.click();
95
+ link.remove();
96
+ URL.revokeObjectURL(url);
97
+ }
98
+
99
+ function readFileAsText(file: File) {
100
+ return new Promise<string>((resolve, reject) => {
101
+ const reader = new FileReader();
102
+ reader.onload = () => resolve(String(reader.result || ""));
103
+ reader.onerror = () => reject(reader.error || new Error("Failed to read file."));
104
+ reader.readAsText(file);
105
+ });
106
+ }
107
+
108
+ function getDownloadFileName(response: Response, fallback: string) {
109
+ const disposition = response.headers.get("content-disposition") || "";
110
+ const match = disposition.match(/filename="?([^";]+)"?/i);
111
+ return match?.[1] || fallback;
112
+ }
113
+
114
+ async function downloadResponseWithProgress(
115
+ response: Response,
116
+ onProgress: (received: number, total: number | null) => void
117
+ ) {
118
+ const total = Number(response.headers.get("content-length") || 0) || null;
119
+ if (!response.body) {
120
+ const blob = await response.blob();
121
+ onProgress(blob.size, total);
122
+ return blob;
123
+ }
124
+
125
+ const reader = response.body.getReader();
126
+ const chunks: Uint8Array[] = [];
127
+ let received = 0;
128
+
129
+ for (;;) {
130
+ const { done, value } = await reader.read();
131
+ if (done) break;
132
+ if (!value) continue;
133
+ chunks.push(value);
134
+ received += value.byteLength;
135
+ onProgress(received, total);
136
+ }
137
+
138
+ return new Blob(chunks as BlobPart[], {
139
+ type: response.headers.get("content-type") || "application/zip",
140
+ });
141
+ }
142
+
143
+ function postFormDataWithProgress<T>(
144
+ url: string,
145
+ formData: FormData,
146
+ onUploadProgress: (loaded: number, total: number | null) => void,
147
+ onUploadComplete?: () => void
148
+ ) {
149
+ return new Promise<{ ok: boolean; status: number; body: T }>((resolve, reject) => {
150
+ const request = new XMLHttpRequest();
151
+ request.open("POST", url);
152
+ request.upload.onprogress = (event) => {
153
+ onUploadProgress(event.loaded, event.lengthComputable ? event.total : null);
154
+ };
155
+ request.upload.onload = () => onUploadComplete?.();
156
+ request.onerror = () => reject(new Error("Network error while uploading backup."));
157
+ request.onload = () => {
158
+ const body = request.responseText ? JSON.parse(request.responseText) : null;
159
+ resolve({
160
+ ok: request.status >= 200 && request.status < 300,
161
+ status: request.status,
162
+ body: body as T,
163
+ });
164
+ };
165
+ request.send(formData);
166
+ });
167
+ }
168
+
169
+ function formatBytes(value?: number) {
170
+ if (!value) return "0 B";
171
+ const units = ["B", "KB", "MB", "GB"];
172
+ let size = value;
173
+ let unitIndex = 0;
174
+ while (size >= 1024 && unitIndex < units.length - 1) {
175
+ size /= 1024;
176
+ unitIndex += 1;
177
+ }
178
+ return `${size.toFixed(size >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
179
+ }
180
+
181
+ function Summary({ summary }: { summary: CmsImportSummary | null }) {
182
+ if (!summary) return null;
183
+
184
+ return (
185
+ <div className="space-y-3">
186
+ <div className="flex flex-wrap gap-2">
187
+ <Badge variant={summary.success ? "default" : "destructive"}>
188
+ {summary.success ? "Ready" : "Needs Fixes"}
189
+ </Badge>
190
+ <Badge variant="outline">{summary.totalRows} rows</Badge>
191
+ <Badge variant="outline">{summary.created} create</Badge>
192
+ <Badge variant="outline">{summary.updated} update</Badge>
193
+ {summary.warnings.length ? (
194
+ <Badge variant="secondary">{summary.warnings.length} warnings</Badge>
195
+ ) : null}
196
+ </div>
197
+ {summary.errors.length ? (
198
+ <Alert variant="destructive">
199
+ <AlertDescription>
200
+ {summary.errors.slice(0, 8).map((error) => (
201
+ <span key={`${error.row}-${error.message}`} className="block">
202
+ Row {error.row}: {error.message}
203
+ </span>
204
+ ))}
205
+ </AlertDescription>
206
+ </Alert>
207
+ ) : null}
208
+ {summary.warnings.length ? (
209
+ <Alert>
210
+ <AlertDescription>
211
+ {summary.warnings.slice(0, 6).map((warning) => (
212
+ <span key={`${warning.row}-${warning.message}`} className="block">
213
+ Row {warning.row}: {warning.message}
214
+ </span>
215
+ ))}
216
+ </AlertDescription>
217
+ </Alert>
218
+ ) : null}
219
+ </div>
220
+ );
221
+ }
222
+
223
+ function FullBackupSummaryPanel({
224
+ summary,
225
+ restored,
226
+ }: {
227
+ summary: FullBackupSummary | null;
228
+ restored: boolean;
229
+ }) {
230
+ if (!summary) return null;
231
+
232
+ return (
233
+ <div className="space-y-3">
234
+ <div className="flex flex-wrap gap-2">
235
+ <Badge variant={summary.success ? "default" : "destructive"}>
236
+ {restored ? "Restored" : summary.success ? "Ready" : "Needs Fixes"}
237
+ </Badge>
238
+ <Badge variant="outline">{summary.storageObjects || 0} R2 objects</Badge>
239
+ <Badge variant="outline">{formatBytes(summary.databaseDumpBytes)} DB</Badge>
240
+ <Badge variant="outline">{formatBytes(summary.contentBundleBytes)} content JSON</Badge>
241
+ {summary.missingStorageObjects ? (
242
+ <Badge variant="destructive">{summary.missingStorageObjects} missing files</Badge>
243
+ ) : null}
244
+ </div>
245
+
246
+ {summary.error ? (
247
+ <Alert variant="destructive">
248
+ <AlertTriangle className="h-4 w-4" />
249
+ <AlertTitle>Full Backup Review Failed</AlertTitle>
250
+ <AlertDescription>{summary.error}</AlertDescription>
251
+ </Alert>
252
+ ) : null}
253
+
254
+ {restored && summary.success ? (
255
+ <Alert variant="success">
256
+ <CheckCircle2 className="h-4 w-4" />
257
+ <AlertTitle>Restoration Completed Successfully</AlertTitle>
258
+ <AlertDescription>
259
+ Uploaded {summary.storageObjectsUploaded || 0} R2 object(s), restored the database,
260
+ and updated {summary.mediaUrlRowsUpdated || 0} media URL row(s).
261
+ {summary.storageObjectsDeleted ? ` Deleted ${summary.storageObjectsDeleted} extra R2 object(s).` : ""}
262
+ </AlertDescription>
263
+ </Alert>
264
+ ) : null}
265
+
266
+ {summary.warnings?.length ? (
267
+ <Alert variant="warning">
268
+ <AlertTriangle className="h-4 w-4" />
269
+ <AlertTitle>Warnings</AlertTitle>
270
+ <AlertDescription>
271
+ {summary.warnings.slice(0, 8).map((warning) => (
272
+ <span key={warning} className="block">
273
+ {warning}
274
+ </span>
275
+ ))}
276
+ </AlertDescription>
277
+ </Alert>
278
+ ) : null}
279
+ </div>
280
+ );
281
+ }
282
+
283
+ function OperationProgressBar({
284
+ progress,
285
+ scope,
286
+ }: {
287
+ progress: OperationProgress | null;
288
+ scope: ProgressScope;
289
+ }) {
290
+ if (!progress || progress.scope !== scope) return null;
291
+
292
+ return (
293
+ <div className="space-y-2 rounded-md border bg-muted/30 p-3">
294
+ <div className="flex items-center justify-between gap-3 text-sm">
295
+ <span className="font-medium">{progress.label}</span>
296
+ <span className="tabular-nums text-muted-foreground">{Math.round(progress.value)}%</span>
297
+ </div>
298
+ <Progress value={progress.value} className="h-2" />
299
+ <p className="text-xs text-muted-foreground">{progress.detail}</p>
300
+ </div>
301
+ );
302
+ }
303
+
304
+ export function BackupRestoreWorkspace({
305
+ isEcommerceActive,
306
+ }: {
307
+ isEcommerceActive: boolean;
308
+ }) {
309
+ const router = useRouter();
310
+ const availableContentTypes = isEcommerceActive
311
+ ? CONTENT_TYPES
312
+ : CONTENT_TYPES.filter((item) => item.value !== "products");
313
+ const contentTypeLabel = isEcommerceActive ? "pages, posts, and products" : "pages and posts";
314
+ const [backupMode, setBackupMode] = useState<BackupMode>("content_json");
315
+ const [bundleJson, setBundleJson] = useState("");
316
+ const [fileName, setFileName] = useState("");
317
+ const [selectedTypes, setSelectedTypes] = useState<CmsContentType[]>(
318
+ isEcommerceActive ? ["pages", "posts", "products"] : ["pages", "posts"]
319
+ );
320
+ const [includeBlocks, setIncludeBlocks] = useState(true);
321
+ const [conflictMode, setConflictMode] =
322
+ useState<CmsImportConflictMode>("overwrite_existing");
323
+ const [applyMode, setApplyMode] = useState<CmsImportApplyMode>("draft");
324
+ const [summary, setSummary] = useState<CmsImportSummary | null>(null);
325
+ const [jsonRestoreComplete, setJsonRestoreComplete] = useState(false);
326
+ const [isPending, startTransition] = useTransition();
327
+ const [operationProgress, setOperationProgress] = useState<OperationProgress | null>(null);
328
+ const progressTimerRef = useRef<number | null>(null);
329
+ const clearProgressTimeoutRef = useRef<number | null>(null);
330
+
331
+ const [fullZipFile, setFullZipFile] = useState<File | null>(null);
332
+ const [fullSummary, setFullSummary] = useState<FullBackupSummary | null>(null);
333
+ const [fullRestoreComplete, setFullRestoreComplete] = useState(false);
334
+ const [isFullPending, setIsFullPending] = useState(false);
335
+ const [mediaUrlMode, setMediaUrlMode] =
336
+ useState<FullBackupMediaUrlMode>("rewrite_to_destination");
337
+ const [destinationBaseUrl, setDestinationBaseUrl] = useState(
338
+ process.env.NEXT_PUBLIC_R2_BASE_URL || ""
339
+ );
340
+ const [deleteExtraneousR2Objects, setDeleteExtraneousR2Objects] = useState(false);
341
+ const [confirmation, setConfirmation] = useState("");
342
+
343
+ const clearProgressTimers = () => {
344
+ if (progressTimerRef.current) {
345
+ window.clearInterval(progressTimerRef.current);
346
+ progressTimerRef.current = null;
347
+ }
348
+ if (clearProgressTimeoutRef.current) {
349
+ window.clearTimeout(clearProgressTimeoutRef.current);
350
+ clearProgressTimeoutRef.current = null;
351
+ }
352
+ };
353
+
354
+ const startProgress = (progress: OperationProgress) => {
355
+ clearProgressTimers();
356
+ setOperationProgress(progress);
357
+ progressTimerRef.current = window.setInterval(() => {
358
+ setOperationProgress((current) => {
359
+ if (!current) return current;
360
+ const remaining = Math.max(0, current.cap - current.value);
361
+ if (remaining <= 0.5) return current;
362
+ return {
363
+ ...current,
364
+ value: Math.min(current.cap, current.value + Math.max(0.7, remaining * 0.07)),
365
+ };
366
+ });
367
+ }, 700) as unknown as number;
368
+ };
369
+
370
+ const updateProgress = (patch: Partial<OperationProgress>) => {
371
+ setOperationProgress((current) => (current ? { ...current, ...patch } : current));
372
+ };
373
+
374
+ const finishProgress = (label: string, detail: string) => {
375
+ clearProgressTimers();
376
+ setOperationProgress((current) =>
377
+ current
378
+ ? {
379
+ ...current,
380
+ label,
381
+ detail,
382
+ value: 100,
383
+ cap: 100,
384
+ }
385
+ : current
386
+ );
387
+ clearProgressTimeoutRef.current = window.setTimeout(() => {
388
+ setOperationProgress(null);
389
+ clearProgressTimeoutRef.current = null;
390
+ }, 1400) as unknown as number;
391
+ };
392
+
393
+ const toggleContentType = (contentType: CmsContentType, checked: boolean) => {
394
+ setSummary(null);
395
+ setJsonRestoreComplete(false);
396
+ setSelectedTypes((current) => {
397
+ if (checked) {
398
+ return Array.from(new Set([...current, contentType]));
399
+ }
400
+
401
+ return current.filter((item) => item !== contentType);
402
+ });
403
+ };
404
+
405
+ const exportBundle = () => {
406
+ startProgress({
407
+ scope: "export",
408
+ label: "Exporting Content JSON",
409
+ detail: `Collecting ${contentTypeLabel} and blocks.`,
410
+ value: 12,
411
+ cap: 82,
412
+ });
413
+ startTransition(async () => {
414
+ const result = await exportCmsBackupBundleAction();
415
+ if (!result.success || !result.content || !result.fileName || !result.mimeType) {
416
+ clearProgressTimers();
417
+ setOperationProgress(null);
418
+ toast.error(result.error || "Failed to export backup.");
419
+ return;
420
+ }
421
+
422
+ updateProgress({
423
+ label: "Downloading Content JSON",
424
+ detail: "Preparing the browser download.",
425
+ value: 92,
426
+ cap: 96,
427
+ });
428
+ downloadTextFile(result.fileName, result.mimeType, result.content);
429
+ finishProgress("Content JSON Exported", "The backup download has started.");
430
+ toast.success("Content JSON backup exported.");
431
+ });
432
+ };
433
+
434
+ const exportFullZip = async () => {
435
+ setIsFullPending(true);
436
+ startProgress({
437
+ scope: "export",
438
+ label: "Building Full Site ZIP",
439
+ detail: "Running database backup and collecting R2 media files.",
440
+ value: 5,
441
+ cap: 64,
442
+ });
443
+ try {
444
+ const response = await fetch("/api/cms/full-backup/export", {
445
+ method: "POST",
446
+ });
447
+
448
+ if (!response.ok) {
449
+ const result = (await response.json().catch(() => null)) as { error?: string } | null;
450
+ throw new Error(result?.error || "Failed to export full ZIP backup.");
451
+ }
452
+
453
+ updateProgress({
454
+ label: "Downloading Full Site ZIP",
455
+ detail: "Receiving the archive from the server.",
456
+ value: 65,
457
+ cap: 96,
458
+ });
459
+ const blob = await downloadResponseWithProgress(response, (received, total) => {
460
+ if (!total) {
461
+ updateProgress({
462
+ value: 84,
463
+ detail: `Downloaded ${formatBytes(received)}.`,
464
+ });
465
+ return;
466
+ }
467
+ updateProgress({
468
+ value: Math.min(98, 65 + (received / total) * 33),
469
+ detail: `Downloaded ${formatBytes(received)} of ${formatBytes(total)}.`,
470
+ });
471
+ });
472
+ downloadBlobFile(
473
+ getDownloadFileName(response, "nextblock-full-site-backup.zip"),
474
+ blob
475
+ );
476
+ finishProgress("Full Site ZIP Exported", "The migration archive download has started.");
477
+ toast.success("Full site ZIP backup exported.");
478
+ } catch (error) {
479
+ clearProgressTimers();
480
+ setOperationProgress(null);
481
+ toast.error(error instanceof Error ? error.message : "Failed to export full ZIP backup.");
482
+ } finally {
483
+ setIsFullPending(false);
484
+ }
485
+ };
486
+
487
+ const reviewImport = () => {
488
+ if (!bundleJson.trim()) {
489
+ toast.error("Choose a backup JSON file first.");
490
+ return;
491
+ }
492
+ if (selectedTypes.length === 0 && !includeBlocks) {
493
+ toast.error("Select at least one content type or the Blocks Library.");
494
+ return;
495
+ }
496
+
497
+ startProgress({
498
+ scope: "restore",
499
+ label: "Reviewing Content Backup",
500
+ detail: "Validating the JSON file before restore.",
501
+ value: 15,
502
+ cap: 86,
503
+ });
504
+ startTransition(async () => {
505
+ const result = await dryRunCmsBackupBundleImportAction({
506
+ bundleJson,
507
+ contentTypes: selectedTypes,
508
+ conflictMode,
509
+ applyMode,
510
+ includeBlocks,
511
+ });
512
+ setSummary(result);
513
+ setJsonRestoreComplete(false);
514
+ if (result.success) {
515
+ finishProgress("Backup Review Complete", "The content backup is ready to restore.");
516
+ toast.success("Backup review is ready.");
517
+ } else {
518
+ clearProgressTimers();
519
+ setOperationProgress(null);
520
+ toast.error("Backup has errors to fix.");
521
+ }
522
+ });
523
+ };
524
+
525
+ const applyImport = () => {
526
+ if (!summary?.success || (selectedTypes.length === 0 && !includeBlocks)) return;
527
+
528
+ startProgress({
529
+ scope: "restore",
530
+ label: "Restoring Content Backup",
531
+ detail: "Writing content records and blocks.",
532
+ value: 12,
533
+ cap: 88,
534
+ });
535
+ startTransition(async () => {
536
+ const result = await applyCmsBackupBundleImportAction({
537
+ bundleJson,
538
+ contentTypes: selectedTypes,
539
+ conflictMode,
540
+ applyMode,
541
+ includeBlocks,
542
+ });
543
+ setSummary(result);
544
+ if (!result.success) {
545
+ setJsonRestoreComplete(false);
546
+ clearProgressTimers();
547
+ setOperationProgress(null);
548
+ toast.error("Backup restore failed validation.");
549
+ return;
550
+ }
551
+
552
+ setJsonRestoreComplete(true);
553
+ finishProgress("Content Restore Complete", "The content backup was restored successfully.");
554
+ toast.success("Restoration completed successfully.");
555
+ router.refresh();
556
+ });
557
+ };
558
+
559
+ const submitFullBackup = async (action: "review" | "restore") => {
560
+ if (!fullZipFile) {
561
+ toast.error("Choose a full backup ZIP file first.");
562
+ return;
563
+ }
564
+ if (action === "restore" && confirmation.trim() !== FULL_RESTORE_CONFIRMATION) {
565
+ toast.error(`Type ${FULL_RESTORE_CONFIRMATION} to confirm full restore.`);
566
+ return;
567
+ }
568
+
569
+ const formData = new FormData();
570
+ formData.set("action", action);
571
+ formData.set("file", fullZipFile);
572
+ formData.set("mediaUrlMode", mediaUrlMode);
573
+ formData.set("destinationBaseUrl", destinationBaseUrl);
574
+ formData.set("deleteExtraneousR2Objects", String(deleteExtraneousR2Objects));
575
+ formData.set("confirmation", confirmation);
576
+
577
+ setIsFullPending(true);
578
+ startProgress({
579
+ scope: "restore",
580
+ label: action === "restore" ? "Uploading Full Backup" : "Uploading Backup for Review",
581
+ detail: "Sending the ZIP archive to the server.",
582
+ value: 8,
583
+ cap: 46,
584
+ });
585
+ try {
586
+ const response = await postFormDataWithProgress<FullBackupSummary>(
587
+ "/api/cms/full-backup/restore",
588
+ formData,
589
+ (loaded, total) => {
590
+ if (!total) {
591
+ updateProgress({
592
+ value: 34,
593
+ detail: `Uploaded ${formatBytes(loaded)}.`,
594
+ });
595
+ return;
596
+ }
597
+ updateProgress({
598
+ value: Math.min(48, 8 + (loaded / total) * 40),
599
+ detail: `Uploaded ${formatBytes(loaded)} of ${formatBytes(total)}.`,
600
+ });
601
+ },
602
+ () => {
603
+ updateProgress({
604
+ label: action === "restore" ? "Restoring Full Backup" : "Reviewing Full Backup",
605
+ detail:
606
+ action === "restore"
607
+ ? "Re-uploading R2 objects, restoring the database, and rewriting media URLs."
608
+ : "Inspecting the archive manifest and required files.",
609
+ value: 52,
610
+ cap: action === "restore" ? 94 : 86,
611
+ });
612
+ }
613
+ );
614
+ const result = response.body;
615
+ setFullSummary(result);
616
+ setFullRestoreComplete(action === "restore" && result.success);
617
+
618
+ if (!response.ok || !result.success) {
619
+ clearProgressTimers();
620
+ setOperationProgress(null);
621
+ toast.error(result.error || "Full backup needs attention.");
622
+ return;
623
+ }
624
+
625
+ finishProgress(
626
+ action === "restore" ? "Full Restore Complete" : "Full Backup Review Complete",
627
+ action === "restore"
628
+ ? "The database and R2 media archive were restored."
629
+ : "The ZIP archive is ready to restore."
630
+ );
631
+ toast.success(
632
+ action === "restore"
633
+ ? "Restoration completed successfully."
634
+ : "Full backup review is ready."
635
+ );
636
+ if (action === "restore") router.refresh();
637
+ } catch (error) {
638
+ clearProgressTimers();
639
+ setOperationProgress(null);
640
+ toast.error(error instanceof Error ? error.message : "Failed to process full backup.");
641
+ } finally {
642
+ setIsFullPending(false);
643
+ }
644
+ };
645
+
646
+ return (
647
+ <div className="mx-auto max-w-6xl space-y-6">
648
+ <div>
649
+ <h1 className="text-2xl font-semibold">Backup And Restore</h1>
650
+ <p className="mt-1 text-sm text-muted-foreground">
651
+ Export content-only JSON backups or full migration ZIP archives with database and R2 files.
652
+ </p>
653
+ </div>
654
+
655
+ <RadioGroup
656
+ value={backupMode}
657
+ onValueChange={(value) => setBackupMode(value as BackupMode)}
658
+ className="grid gap-3 md:grid-cols-2"
659
+ >
660
+ <label className="flex cursor-pointer items-start gap-3 rounded-md border p-4">
661
+ <RadioGroupItem value="content_json" className="mt-1" />
662
+ <span className="space-y-1">
663
+ <span className="block text-sm font-medium">Content JSON</span>
664
+ <span className="block text-sm text-muted-foreground">
665
+ {isEcommerceActive
666
+ ? "Pages, posts, products, metadata, translation groups, blocks, product variants, category slugs, media references, and custom block definitions (Blocks Library). Image binaries are not included."
667
+ : "Pages, posts, metadata, translation groups, blocks, and custom block definitions (Blocks Library). Product content is included only when the ecommerce package is active. Image binaries are not included."}
668
+ </span>
669
+ </span>
670
+ </label>
671
+ <label className="flex cursor-pointer items-start gap-3 rounded-md border p-4">
672
+ <RadioGroupItem value="full_zip" className="mt-1" />
673
+ <span className="space-y-1">
674
+ <span className="block text-sm font-medium">Full Site ZIP</span>
675
+ <span className="block text-sm text-muted-foreground">
676
+ Migration archive with a database dump and R2 media files. Restoring this is destructive
677
+ and intended for moving an old server to a new one.
678
+ </span>
679
+ </span>
680
+ </label>
681
+ </RadioGroup>
682
+
683
+ <div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
684
+ <Card>
685
+ <CardHeader>
686
+ <CardTitle>Export Backup</CardTitle>
687
+ <CardDescription>
688
+ {backupMode === "content_json"
689
+ ? `Download an editable JSON bundle for CMS ${contentTypeLabel}.`
690
+ : "Download a full migration ZIP with database dump and R2 media objects."}
691
+ </CardDescription>
692
+ </CardHeader>
693
+ <CardContent className="space-y-4">
694
+ {backupMode === "content_json" ? (
695
+ <>
696
+ <Alert>
697
+ <AlertDescription>
698
+ The JSON file references existing media by ID and object key, but does not include
699
+ the image or file binaries from R2.
700
+ </AlertDescription>
701
+ </Alert>
702
+ <Button onClick={exportBundle} disabled={isPending}>
703
+ {isPending ? <Spinner className="mr-2 h-4 w-4" /> : <Download className="mr-2 h-4 w-4" />}
704
+ Export Content JSON
705
+ </Button>
706
+ <OperationProgressBar progress={operationProgress} scope="export" />
707
+ </>
708
+ ) : (
709
+ <>
710
+ <Alert variant="warning">
711
+ <AlertTriangle className="h-4 w-4" />
712
+ <AlertTitle>Sensitive Migration Archive</AlertTitle>
713
+ <AlertDescription>
714
+ The ZIP can include users, orders, customer data, payment records, settings,
715
+ database schema, and media files depending on your database permissions.
716
+ </AlertDescription>
717
+ </Alert>
718
+ <Button onClick={exportFullZip} disabled={isFullPending}>
719
+ {isFullPending ? <Spinner className="mr-2 h-4 w-4" /> : <Download className="mr-2 h-4 w-4" />}
720
+ Export Full Site ZIP
721
+ </Button>
722
+ <OperationProgressBar progress={operationProgress} scope="export" />
723
+ </>
724
+ )}
725
+ </CardContent>
726
+ </Card>
727
+
728
+ <Card>
729
+ <CardHeader>
730
+ <CardTitle>Restore from Backup</CardTitle>
731
+ <CardDescription>
732
+ {backupMode === "content_json"
733
+ ? `Review a content backup before restoring ${contentTypeLabel}.`
734
+ : "Review a full ZIP first, then restore database and R2 media into this environment."}
735
+ </CardDescription>
736
+ </CardHeader>
737
+ <CardContent className="space-y-5">
738
+ {backupMode === "content_json" ? (
739
+ <>
740
+ <div className="space-y-2">
741
+ <Label htmlFor="backup-json-file">Backup JSON file</Label>
742
+ <Input
743
+ id="backup-json-file"
744
+ type="file"
745
+ accept=".json,application/json"
746
+ onChange={async (event) => {
747
+ const file = event.target.files?.[0];
748
+ setSummary(null);
749
+ setJsonRestoreComplete(false);
750
+ if (!file) return;
751
+ setFileName(file.name);
752
+ try {
753
+ setBundleJson(await readFileAsText(file));
754
+ } catch {
755
+ toast.error("Failed to read backup file.");
756
+ }
757
+ }}
758
+ />
759
+ {fileName ? (
760
+ <p className="text-xs text-muted-foreground">{fileName}</p>
761
+ ) : null}
762
+ </div>
763
+
764
+ <div className="grid gap-3 sm:grid-cols-3">
765
+ {availableContentTypes.map((item) => (
766
+ <label
767
+ key={item.value}
768
+ className="flex items-center gap-2 rounded-md border px-3 py-2 text-sm"
769
+ >
770
+ <Checkbox
771
+ checked={selectedTypes.includes(item.value)}
772
+ onCheckedChange={(checked) => toggleContentType(item.value, checked === true)}
773
+ />
774
+ {item.label}
775
+ </label>
776
+ ))}
777
+ <label className="flex items-center gap-2 rounded-md border px-3 py-2 text-sm">
778
+ <Checkbox
779
+ checked={includeBlocks}
780
+ onCheckedChange={(checked) => {
781
+ setIncludeBlocks(checked === true);
782
+ setSummary(null);
783
+ setJsonRestoreComplete(false);
784
+ }}
785
+ />
786
+ Blocks Library
787
+ </label>
788
+ </div>
789
+
790
+ <div className="grid gap-3 sm:grid-cols-2">
791
+ <div className="space-y-2">
792
+ <Label>Existing records</Label>
793
+ <Select
794
+ value={conflictMode}
795
+ onValueChange={(value) => {
796
+ setConflictMode(value as CmsImportConflictMode);
797
+ setSummary(null);
798
+ setJsonRestoreComplete(false);
799
+ }}
800
+ >
801
+ <SelectTrigger>
802
+ <SelectValue />
803
+ </SelectTrigger>
804
+ <SelectContent>
805
+ <SelectItem value="overwrite_existing">Overwrite matches</SelectItem>
806
+ <SelectItem value="create_new">Create new copies</SelectItem>
807
+ </SelectContent>
808
+ </Select>
809
+ </div>
810
+
811
+ <div className="space-y-2">
812
+ <Label>Apply as</Label>
813
+ <Select
814
+ value={applyMode}
815
+ onValueChange={(value) => {
816
+ setApplyMode(value as CmsImportApplyMode);
817
+ setSummary(null);
818
+ setJsonRestoreComplete(false);
819
+ }}
820
+ >
821
+ <SelectTrigger>
822
+ <SelectValue />
823
+ </SelectTrigger>
824
+ <SelectContent>
825
+ <SelectItem value="draft">Drafts</SelectItem>
826
+ <SelectItem value="live">Live content</SelectItem>
827
+ </SelectContent>
828
+ </Select>
829
+ </div>
830
+ </div>
831
+
832
+ <Summary summary={summary} />
833
+ <OperationProgressBar progress={operationProgress} scope="restore" />
834
+
835
+ {jsonRestoreComplete && summary?.success ? (
836
+ <Alert variant="success">
837
+ <CheckCircle2 className="h-4 w-4" />
838
+ <AlertTitle>Restoration Completed Successfully</AlertTitle>
839
+ <AlertDescription>
840
+ Restored {summary.totalRows} content row(s): {summary.created} created and{" "}
841
+ {summary.updated} updated.
842
+ </AlertDescription>
843
+ </Alert>
844
+ ) : null}
845
+
846
+ <div className="flex flex-wrap gap-2">
847
+ <Button
848
+ type="button"
849
+ variant="outline"
850
+ onClick={reviewImport}
851
+ disabled={
852
+ isPending ||
853
+ !bundleJson.trim() ||
854
+ (selectedTypes.length === 0 && !includeBlocks)
855
+ }
856
+ >
857
+ {isPending ? <Spinner className="mr-2 h-4 w-4" /> : <Upload className="mr-2 h-4 w-4" />}
858
+ Review Backup
859
+ </Button>
860
+ <Button
861
+ type="button"
862
+ onClick={applyImport}
863
+ disabled={
864
+ isPending ||
865
+ !summary?.success ||
866
+ (selectedTypes.length === 0 && !includeBlocks)
867
+ }
868
+ >
869
+ {isPending ? <Spinner className="mr-2 h-4 w-4" /> : <Upload className="mr-2 h-4 w-4" />}
870
+ Restore from Backup
871
+ </Button>
872
+ </div>
873
+ </>
874
+ ) : (
875
+ <>
876
+ <Alert variant="warning">
877
+ <AlertTriangle className="h-4 w-4" />
878
+ <AlertTitle>Full Restore Replaces This Site</AlertTitle>
879
+ <AlertDescription>
880
+ This restores R2 files and runs the archived SQL dump against the configured database.
881
+ Use it for migration from an old server to a new server.
882
+ </AlertDescription>
883
+ </Alert>
884
+
885
+ <div className="space-y-2">
886
+ <Label htmlFor="backup-zip-file">Full backup ZIP file</Label>
887
+ <Input
888
+ id="backup-zip-file"
889
+ type="file"
890
+ accept=".zip,application/zip"
891
+ onChange={(event) => {
892
+ const file = event.target.files?.[0] || null;
893
+ setFullZipFile(file);
894
+ setFullSummary(null);
895
+ setFullRestoreComplete(false);
896
+ }}
897
+ />
898
+ {fullZipFile ? (
899
+ <p className="text-xs text-muted-foreground">
900
+ {fullZipFile.name} ({formatBytes(fullZipFile.size)})
901
+ </p>
902
+ ) : null}
903
+ </div>
904
+
905
+ <div className="grid gap-3 sm:grid-cols-2">
906
+ <div className="space-y-2">
907
+ <Label>Media URL mode</Label>
908
+ <Select
909
+ value={mediaUrlMode}
910
+ onValueChange={(value) => {
911
+ setMediaUrlMode(value as FullBackupMediaUrlMode);
912
+ setFullRestoreComplete(false);
913
+ }}
914
+ >
915
+ <SelectTrigger>
916
+ <SelectValue />
917
+ </SelectTrigger>
918
+ <SelectContent>
919
+ <SelectItem value="rewrite_to_destination">Use new bucket address</SelectItem>
920
+ <SelectItem value="keep_exact_urls">Keep exact public URLs</SelectItem>
921
+ </SelectContent>
922
+ </Select>
923
+ </div>
924
+ <div className="space-y-2">
925
+ <Label htmlFor="destination-base-url">New media base URL</Label>
926
+ <Input
927
+ id="destination-base-url"
928
+ value={destinationBaseUrl}
929
+ onChange={(event) => {
930
+ setDestinationBaseUrl(event.target.value);
931
+ setFullRestoreComplete(false);
932
+ }}
933
+ placeholder="https://assets.example.com"
934
+ disabled={mediaUrlMode === "keep_exact_urls"}
935
+ />
936
+ </div>
937
+ </div>
938
+
939
+ <label className="flex items-start gap-2 rounded-md border p-3 text-sm">
940
+ <Checkbox
941
+ checked={deleteExtraneousR2Objects}
942
+ onCheckedChange={(checked) => {
943
+ setDeleteExtraneousR2Objects(checked === true);
944
+ setFullRestoreComplete(false);
945
+ }}
946
+ />
947
+ <span>
948
+ <span className="block font-medium">Delete destination R2 files not in the archive</span>
949
+ <span className="block text-muted-foreground">
950
+ Advanced cleanup. Leave off unless this new bucket is dedicated to the restored site.
951
+ </span>
952
+ </span>
953
+ </label>
954
+
955
+ <FullBackupSummaryPanel summary={fullSummary} restored={fullRestoreComplete} />
956
+ <OperationProgressBar progress={operationProgress} scope="restore" />
957
+
958
+ <Separator />
959
+
960
+ <div className="space-y-2">
961
+ <Label htmlFor="full-restore-confirmation">Destructive restore confirmation</Label>
962
+ <Input
963
+ id="full-restore-confirmation"
964
+ value={confirmation}
965
+ onChange={(event) => setConfirmation(event.target.value)}
966
+ placeholder={FULL_RESTORE_CONFIRMATION}
967
+ />
968
+ <p className="text-xs text-muted-foreground">
969
+ Type {FULL_RESTORE_CONFIRMATION} before restoring the full ZIP.
970
+ </p>
971
+ </div>
972
+
973
+ <div className="flex flex-wrap gap-2">
974
+ <Button
975
+ type="button"
976
+ variant="outline"
977
+ onClick={() => submitFullBackup("review")}
978
+ disabled={isFullPending || !fullZipFile}
979
+ >
980
+ {isFullPending ? <Spinner className="mr-2 h-4 w-4" /> : <Upload className="mr-2 h-4 w-4" />}
981
+ Review Full Backup
982
+ </Button>
983
+ <Button
984
+ type="button"
985
+ onClick={() => submitFullBackup("restore")}
986
+ disabled={
987
+ isFullPending ||
988
+ !fullSummary?.success ||
989
+ fullRestoreComplete ||
990
+ confirmation.trim() !== FULL_RESTORE_CONFIRMATION
991
+ }
992
+ >
993
+ {isFullPending ? <Spinner className="mr-2 h-4 w-4" /> : <Upload className="mr-2 h-4 w-4" />}
994
+ Restore from Backup
995
+ </Button>
996
+ </div>
997
+ </>
998
+ )}
999
+ </CardContent>
1000
+ </Card>
1001
+ </div>
1002
+ </div>
1003
+ );
1004
+ }