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
@@ -1,9 +1,8 @@
1
1
  // app/cms/posts/components/PostForm.tsx
2
2
  "use client";
3
3
 
4
- import { useEffect, useState, useTransition, useCallback } from "react";
4
+ import { useEffect, useRef, useState, useTransition } from "react";
5
5
  import { useRouter, useSearchParams } from "next/navigation";
6
- import Image from "next/image";
7
6
  import { Button } from "@nextblock-cms/ui";
8
7
  import { Input } from "@nextblock-cms/ui";
9
8
  import { Label } from "@nextblock-cms/ui";
@@ -16,39 +15,46 @@ import {
16
15
  } from "@nextblock-cms/ui";
17
16
  import { Spinner, Alert, AlertDescription } from "@nextblock-cms/ui";
18
17
  import { Textarea } from "@nextblock-cms/ui";
19
- import {
20
- Dialog,
21
- DialogContent,
22
- DialogHeader,
23
- DialogTitle,
24
- DialogTrigger,
25
- DialogFooter,
26
- DialogClose,
27
- } from "@nextblock-cms/ui";
28
18
  import type { Database } from "@nextblock-cms/db";
29
- import { useAuth } from "@/context/AuthContext";
19
+ import { useAuth } from '../../../../context/AuthContext';
20
+ import FeatureImageField from "../../components/FeatureImageField";
30
21
 
31
22
  type Post = Database['public']['Tables']['posts']['Row'];
32
23
  type PageStatus = Database['public']['Enums']['page_status'];
33
24
  type Language = Database['public']['Tables']['languages']['Row'];
34
- type Media = Database['public']['Tables']['media']['Row'];
35
- // import MediaGridClient from "@/app/cms/media/components/MediaGridClient"; // Will render a custom grid instead
36
- import MediaImage from "@/app/cms/media/components/MediaImage"; // For displaying images in the modal
37
- import { getMediaItems } from "@/app/cms/media/actions";
38
- import MediaUploadForm from "@/app/cms/media/components/MediaUploadForm";
39
- import { Separator } from "@nextblock-cms/ui";
40
- import { useRef } from "react";
41
- import { useHotkeys } from "@/hooks/use-hotkeys";
25
+ import { useHotkeys } from '../../../../hooks/use-hotkeys';
42
26
 
43
27
 
44
28
  interface PostFormProps {
45
- post?: Post & { feature_image_id?: string | null }; // Assuming feature_image_id can be string
29
+ post?: Post & { feature_image_id?: string | null };
46
30
  formAction: (formData: FormData) => Promise<{ error?: string } | void>;
47
31
  actionButtonText?: string;
48
32
  isEditing?: boolean;
49
33
  availableLanguagesProp?: Language[]; // Make optional
50
34
  initialFeatureImageUrl?: string | null;
51
- initialFeatureImageId?: string | null; // Pass initial ID as string
35
+ initialFeatureImageId?: string | null;
36
+ }
37
+
38
+ function formatDateTimeLocal(value: string | null | undefined) {
39
+ if (!value) {
40
+ return "";
41
+ }
42
+
43
+ try {
44
+ const date = new Date(value);
45
+ if (Number.isNaN(date.getTime())) {
46
+ return "";
47
+ }
48
+
49
+ const year = date.getFullYear();
50
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
51
+ const day = date.getDate().toString().padStart(2, '0');
52
+ const hours = date.getHours().toString().padStart(2, '0');
53
+ const minutes = date.getMinutes().toString().padStart(2, '0');
54
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
55
+ } catch {
56
+ return "";
57
+ }
52
58
  }
53
59
 
54
60
  export default function PostForm({
@@ -67,108 +73,35 @@ export default function PostForm({
67
73
 
68
74
  const [title, setTitle] = useState(post?.title || "");
69
75
  const [slug, setSlug] = useState(post?.slug || "");
76
+ const [label, setLabel] = useState(post?.label || "");
70
77
  const [languageId, setLanguageId] = useState<string>(
71
78
  post?.language_id?.toString() || ""
72
79
  );
73
80
  const [status, setStatus] = useState<PageStatus>(post?.status || "draft");
74
81
  const [excerpt, setExcerpt] = useState(post?.excerpt || "");
75
- const [publishedAt, setPublishedAt] = useState<string>(() => {
76
- if (post?.published_at) {
77
- try {
78
- const date = new Date(post.published_at);
79
- const year = date.getFullYear();
80
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
81
- const day = date.getDate().toString().padStart(2, '0');
82
- const hours = date.getHours().toString().padStart(2, '0');
83
- const minutes = date.getMinutes().toString().padStart(2, '0');
84
- return `${year}-${month}-${day}T${hours}:${minutes}`;
85
- } catch {
86
- return "";
87
- }
88
- }
89
- return "";
90
- });
91
- const [metaTitle, setMetaTitle] = useState(post?.meta_title || "");
92
- const [metaDescription, setMetaDescription] = useState(
93
- post?.meta_description || ""
82
+ const [subtitle, setSubtitle] = useState(post?.subtitle || "");
83
+ const [publishedAt, setPublishedAt] = useState<string>(() =>
84
+ formatDateTimeLocal(post?.published_at)
94
85
  );
86
+ const [metaTitle, setMetaTitle] = useState(post?.meta_title || "");
87
+ const [metaDescription, setMetaDescription] = useState(
88
+ post?.meta_description || ""
89
+ );
90
+ const [featureImageId, setFeatureImageId] = useState<string | null>(
91
+ initialFeatureImageId || post?.feature_image_id || null
92
+ );
95
93
 
96
94
  // Use the passed-in languages directly
97
95
  const [availableLanguages] = useState<Language[]>(availableLanguagesProp);
98
96
 
99
- const [selectedFeatureImage, setSelectedFeatureImage] = useState<{ id: string | null; url: string | null }>({
100
- id: initialFeatureImageId || post?.feature_image_id || null, // Prioritize prop, then post data
101
- url: initialFeatureImageUrl || null,
102
- });
103
- const [isModalOpen, setIsModalOpen] = useState(false);
104
- const [mediaItems, setMediaItems] = useState<Media[]>([]);
105
- const [mediaLoading, setMediaLoading] = useState(false);
106
- const [mediaError, setMediaError] = useState<string | null>(null);
107
- const [mediaPage, setMediaPage] = useState(1);
108
- const [hasMoreMedia, setHasMoreMedia] = useState(true);
109
-
110
97
  const [formMessage, setFormMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
98
+ const formRef = useRef<HTMLFormElement>(null);
99
+ const [isSaving, setIsSaving] = useState(false);
100
+ const [saveError, setSaveError] = useState<string | null>(null);
101
+ const [lastSaved, setLastSaved] = useState<Date | null>(null);
102
+ const isFirstRender = useRef(true);
111
103
 
112
- useEffect(() => {
113
- // Update selectedFeatureImage if initial props change
114
- setSelectedFeatureImage({
115
- id: initialFeatureImageId || post?.feature_image_id || null,
116
- url: initialFeatureImageUrl || null,
117
- });
118
- }, [initialFeatureImageId, initialFeatureImageUrl, post?.feature_image_id]);
119
-
120
- const loadMedia = useCallback(async (pageToLoad = 1, append = false) => {
121
- if (!hasMoreMedia && append && pageToLoad > mediaPage) return;
122
- setMediaLoading(true);
123
- setMediaError(null);
124
- try {
125
- const result = await getMediaItems(pageToLoad, 20); // Fetch 20 items per page
126
- if (result.error) {
127
- setMediaError(result.error);
128
- if (!append) setMediaItems([]); // Clear if not appending on error
129
- } else if (result.data) {
130
- setMediaItems(prev => append ? [...prev, ...(result.data || [])] : (result.data || []));
131
- setHasMoreMedia(result.hasMore !== undefined ? result.hasMore : false);
132
- setMediaPage(pageToLoad);
133
- }
134
- } catch {
135
- setMediaError("An unexpected error occurred while fetching media.");
136
- if (!append) setMediaItems([]);
137
- } finally {
138
- setMediaLoading(false);
139
- }
140
- }, [hasMoreMedia, mediaPage]);
141
-
142
- // Load initial media when modal is opened
143
- useEffect(() => {
144
- if (isModalOpen) {
145
- // Reset and load fresh if opening modal, or if mediaItems is empty
146
- if (mediaItems.length === 0 || !hasMoreMedia || mediaPage !==1) {
147
- setMediaPage(1);
148
- setHasMoreMedia(true); // Assume there might be more media on fresh open
149
- loadMedia(1, false);
150
- }
151
- }
152
- }, [isModalOpen, hasMoreMedia, loadMedia, mediaItems.length, mediaPage]);
153
-
154
- const handleImageSelectInModal = (image: Media) => {
155
- const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
156
- if (!r2BaseUrl) {
157
- console.error("NEXT_PUBLIC_R2_PUBLIC_URL is not set. Cannot construct image URL.");
158
- setMediaError("Image server configuration is missing. Cannot display images.");
159
- return;
160
- }
161
- const imageUrl = image.object_key ? `${r2BaseUrl}/${image.object_key}` : null;
162
-
163
- if (!imageUrl) {
164
- console.error("Selected image does not have an object_key:", image);
165
- setMediaError("Selected image is missing a valid identifier.");
166
- return;
167
- }
168
-
169
- setSelectedFeatureImage({ id: image.id, url: imageUrl }); // image.id is already string (uuid)
170
- setIsModalOpen(false);
171
- };
104
+ useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
172
105
 
173
106
 
174
107
  useEffect(() => {
@@ -181,6 +114,38 @@ export default function PostForm({
181
114
  }
182
115
  }, [searchParams]);
183
116
 
117
+ useEffect(() => {
118
+ if (!post) {
119
+ return;
120
+ }
121
+
122
+ setTitle(post.title || "");
123
+ setSlug(post.slug || "");
124
+ setLabel(post.label || "");
125
+ setLanguageId(post.language_id?.toString() || "");
126
+ setStatus(post.status || "draft");
127
+ setExcerpt(post.excerpt || "");
128
+ setSubtitle(post.subtitle || "");
129
+ setPublishedAt(formatDateTimeLocal(post.published_at));
130
+ setMetaTitle(post.meta_title || "");
131
+ setMetaDescription(post.meta_description || "");
132
+ setFeatureImageId(initialFeatureImageId || post.feature_image_id || null);
133
+ }, [
134
+ initialFeatureImageId,
135
+ post?.excerpt,
136
+ post?.id,
137
+ post?.label,
138
+ post?.language_id,
139
+ post?.meta_description,
140
+ post?.meta_title,
141
+ post?.published_at,
142
+ post?.slug,
143
+ post?.status,
144
+ post?.subtitle,
145
+ post?.title,
146
+ post?.updated_at,
147
+ ]);
148
+
184
149
  // Initialize languageId if creating new post and languages are available
185
150
  useEffect(() => {
186
151
  if (!isEditing && availableLanguages.length > 0 && !languageId) { // check !isEditing too
@@ -200,20 +165,110 @@ export default function PostForm({
200
165
  }
201
166
  };
202
167
 
203
- const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
204
- event.preventDefault();
205
- setFormMessage(null);
206
- const formData = new FormData(event.currentTarget);
168
+ const saveDraft = async (customFormData?: FormData) => {
169
+ if (!title.trim() || !slug.trim()) {
170
+ return;
171
+ }
172
+ setIsSaving(true);
173
+ setSaveError(null);
174
+
175
+ const formData = customFormData || (formRef.current ? new FormData(formRef.current) : new FormData());
176
+ if (!customFormData && !formRef.current) {
177
+ formData.append("title", title);
178
+ formData.append("slug", slug);
179
+ formData.append("language_id", languageId);
180
+ formData.append("label", label);
181
+ formData.append("status", status);
182
+ formData.append("excerpt", excerpt);
183
+ formData.append("subtitle", subtitle);
184
+ formData.append("published_at", publishedAt);
185
+ formData.append("meta_title", metaTitle);
186
+ formData.append("meta_description", metaDescription);
187
+ formData.append("feature_image_id", featureImageId || "");
188
+ }
207
189
 
208
- startTransition(async () => {
190
+ try {
209
191
  const result = await formAction(formData);
210
- if (result?.error) {
192
+ if (result && 'error' in result && result.error) {
193
+ setSaveError(result.error);
211
194
  setFormMessage({ type: 'error', text: result.error });
195
+ } else {
196
+ setLastSaved(new Date());
197
+ setFormMessage(null);
198
+ router.refresh();
212
199
  }
213
- // Success is handled by redirect with query param in server action
214
- });
200
+ } catch (err: any) {
201
+ const msg = err.message || "Failed to save draft";
202
+ setSaveError(msg);
203
+ setFormMessage({ type: 'error', text: msg });
204
+ } finally {
205
+ setIsSaving(false);
206
+ }
207
+ };
208
+
209
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
210
+ event.preventDefault();
211
+ if (!isEditing) {
212
+ setFormMessage(null);
213
+ const formData = new FormData(event.currentTarget);
214
+
215
+ startTransition(async () => {
216
+ const result = await formAction(formData);
217
+ if (result?.error) {
218
+ setFormMessage({ type: 'error', text: result.error });
219
+ }
220
+ });
221
+ } else {
222
+ await saveDraft();
223
+ }
215
224
  };
216
225
 
226
+ useEffect(() => {
227
+ if (!isEditing) return;
228
+
229
+ if (isFirstRender.current) {
230
+ isFirstRender.current = false;
231
+ return;
232
+ }
233
+
234
+ const dbPublishedAt = formatDateTimeLocal(post?.published_at);
235
+
236
+ const hasChanges =
237
+ title !== (post?.title || "") ||
238
+ slug !== (post?.slug || "") ||
239
+ label !== (post?.label || "") ||
240
+ languageId !== (post?.language_id?.toString() || "") ||
241
+ status !== (post?.status || "draft") ||
242
+ excerpt !== (post?.excerpt || "") ||
243
+ subtitle !== (post?.subtitle || "") ||
244
+ publishedAt !== dbPublishedAt ||
245
+ metaTitle !== (post?.meta_title || "") ||
246
+ metaDescription !== (post?.meta_description || "") ||
247
+ featureImageId !== (post?.feature_image_id || null);
248
+
249
+ if (!hasChanges) return;
250
+
251
+ const timer = setTimeout(() => {
252
+ saveDraft();
253
+ }, 1000);
254
+
255
+ return () => clearTimeout(timer);
256
+ }, [
257
+ title,
258
+ slug,
259
+ label,
260
+ languageId,
261
+ status,
262
+ excerpt,
263
+ subtitle,
264
+ publishedAt,
265
+ metaTitle,
266
+ metaDescription,
267
+ featureImageId,
268
+ post,
269
+ isEditing,
270
+ ]);
271
+
217
272
  // Remove languagesLoading from this condition
218
273
  if (authLoading) {
219
274
  return <div>Loading form...</div>;
@@ -222,11 +277,32 @@ export default function PostForm({
222
277
  return <div>Please log in to manage posts.</div>;
223
278
  }
224
279
 
225
- const formRef = useRef<HTMLFormElement>(null);
226
- useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
227
-
228
280
  return (
229
281
  <form ref={formRef} onSubmit={handleSubmit} className="space-y-6 w-full mx-auto px-6">
282
+ {isEditing && (
283
+ <div className="flex items-center justify-between text-xs text-muted-foreground pb-2 border-b border-border/40 mb-2">
284
+ <span className="font-semibold text-[11px] uppercase tracking-wider text-muted-foreground/80">Post Settings</span>
285
+ <div className="flex items-center gap-1.5 min-h-[16px]">
286
+ {isSaving ? (
287
+ <>
288
+ <div className="relative flex h-2 w-2">
289
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
290
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-amber-500"></span>
291
+ </div>
292
+ <span className="text-amber-600 dark:text-amber-400 font-medium">Autosaving settings...</span>
293
+ </>
294
+ ) : saveError ? (
295
+ <span className="text-red-500 font-medium">Error saving settings: {saveError}</span>
296
+ ) : lastSaved ? (
297
+ <span className="text-emerald-600 dark:text-emerald-400 font-medium">
298
+ Settings autosaved at {lastSaved.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
299
+ </span>
300
+ ) : (
301
+ <span className="text-muted-foreground/60">Settings autosave in draft mode</span>
302
+ )}
303
+ </div>
304
+ </div>
305
+ )}
230
306
  {formMessage && (
231
307
  <Alert variant={formMessage.type === 'success' ? 'success' : 'destructive'} className="mb-4">
232
308
  <AlertDescription>{formMessage.text}</AlertDescription>
@@ -258,9 +334,45 @@ export default function PostForm({
258
334
  )}
259
335
  </div>
260
336
 
337
+ <div>
338
+ <Label htmlFor="label">Label</Label>
339
+ <Input
340
+ id="label"
341
+ name="label"
342
+ value={label}
343
+ onChange={(e) => setLabel(e.target.value)}
344
+ className="mt-1"
345
+ placeholder="e.g. Architecture"
346
+ />
347
+ <p className="text-xs text-muted-foreground mt-1">Short pill text shown on the article hero and post cards.</p>
348
+ </div>
349
+
261
350
  <div>
262
351
  <Label htmlFor="excerpt">Excerpt</Label>
263
- <Textarea id="excerpt" name="excerpt" value={excerpt} onChange={(e) => setExcerpt(e.target.value)} className="mt-1" rows={3} />
352
+ <Textarea
353
+ id="excerpt"
354
+ name="excerpt"
355
+ value={excerpt}
356
+ onChange={(e) => setExcerpt(e.target.value)}
357
+ className="mt-1"
358
+ rows={3}
359
+ placeholder="Short editorial summary for the hero metadata row and article cards"
360
+ />
361
+ <p className="text-xs text-muted-foreground mt-1">Used as the short summary above the hero and on public post cards.</p>
362
+ </div>
363
+
364
+ <div>
365
+ <Label htmlFor="subtitle">Subtitle</Label>
366
+ <Textarea
367
+ id="subtitle"
368
+ name="subtitle"
369
+ value={subtitle}
370
+ onChange={(e) => setSubtitle(e.target.value)}
371
+ className="mt-1"
372
+ rows={4}
373
+ placeholder="Longer deck shown under the article title"
374
+ />
375
+ <p className="text-xs text-muted-foreground mt-1">Displayed as the larger deck under the article title.</p>
264
376
  </div>
265
377
 
266
378
  <div>
@@ -298,128 +410,27 @@ export default function PostForm({
298
410
  <Textarea id="meta_description" name="meta_description" value={metaDescription} onChange={(e) => setMetaDescription(e.target.value)} className="mt-1" rows={3} />
299
411
  </div>
300
412
 
301
- {/* Feature Image Selection */}
302
- <div>
303
- <Label htmlFor="feature_image">Feature Image</Label>
304
- <Input type="hidden" name="feature_image_id" value={selectedFeatureImage.id || ""} />
305
- <div className="mt-2">
306
- {selectedFeatureImage.url && (
307
- <div className="mb-4">
308
- <Image
309
- src={selectedFeatureImage.url}
310
- alt="Selected feature image"
311
- width={200}
312
- height={200}
313
- className="rounded-md object-cover"
314
- />
315
- <Button
316
- type="button"
317
- variant="link"
318
- className="mt-2 text-red-600 px-0"
319
- onClick={() => setSelectedFeatureImage({ id: null, url: null })}
320
- >
321
- Remove Image
322
- </Button>
323
- </div>
324
- )}
325
- <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
326
- <DialogTrigger asChild>
327
- <Button type="button" variant="outline">
328
- {selectedFeatureImage.id ? "Change Feature Image" : "Select Feature Image"}
329
- </Button>
330
- </DialogTrigger>
331
- <DialogContent className="sm:max-w-[90vw] max-h-[90vh] flex flex-col">
332
- <DialogHeader>
333
- <DialogTitle>Select Feature Image</DialogTitle>
334
- </DialogHeader>
335
- <div className="p-1">
336
- <MediaUploadForm
337
- returnJustData={true}
338
- defaultFolder={`posts/${(slug || 'untitled').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '')}/`}
339
- onUploadSuccess={(newlyUploadedMedia) => {
340
- setMediaItems(prevItems => [newlyUploadedMedia, ...prevItems.filter(item => item.id !== newlyUploadedMedia.id)]);
341
- handleImageSelectInModal(newlyUploadedMedia);
342
- }}
343
- />
344
- </div>
345
- <Separator className="my-4" />
346
- <div className="py-4 flex-grow overflow-y-auto" id="media-modal-scroll-area">
347
- {mediaLoading && mediaItems.length === 0 && <p className="text-center text-muted-foreground">Loading media...</p>}
348
- {mediaError && <p className="text-red-600 text-center">{mediaError}</p>}
349
- {!mediaLoading && !mediaError && mediaItems.length === 0 && <p className="text-center text-muted-foreground">No media items found. Try uploading some first.</p>}
350
-
351
- {mediaItems.length > 0 && (
352
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 gap-3">
353
- {mediaItems.map((item) => {
354
- const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
355
- if (!r2BaseUrl && item.object_key) {
356
- // This check is more for safety, error primarily handled in handleImageSelectInModal
357
- if (!mediaError) setMediaError("Image server configuration is missing. Cannot display images.");
358
- return null; // Or a placeholder
359
- }
360
- const imageUrl = item.object_key ? `${r2BaseUrl}/${item.object_key}` : null;
361
-
362
- // Only render image-type media for selection
363
- if (!item.file_type?.startsWith("image/") || !imageUrl) {
364
- return null;
365
- }
366
-
367
- return (
368
- <div
369
- key={item.id}
370
- className="group relative border rounded-lg overflow-hidden shadow-sm aspect-square bg-muted/20 transition-all cursor-pointer hover:ring-2 hover:ring-primary"
371
- onClick={() => handleImageSelectInModal(item)}
372
- onKeyDown={(e) => e.key === 'Enter' && handleImageSelectInModal(item)}
373
- tabIndex={0}
374
- role="button"
375
- aria-label={`Select ${item.file_name}`}
376
- >
377
- <MediaImage
378
- src={imageUrl}
379
- alt={item.description || item.file_name}
380
- width={item.width || 300} // Provide a fallback or ensure width is always present
381
- height={item.height || 300} // Provide a fallback or ensure height is always present
382
- blurDataURL={item.blur_data_url}
383
- className="h-full w-full object-cover transition-transform group-hover:scale-105"
384
- />
385
- <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-2">
386
- <p className="text-xs text-white truncate" title={item.file_name}>{item.file_name}</p>
387
- </div>
388
- </div>
389
- );
390
- })}
391
- </div>
392
- )}
393
- {!mediaLoading && hasMoreMedia && mediaItems.length > 0 && (
394
- <div className="text-center mt-6">
395
- <Button onClick={() => loadMedia(mediaPage + 1, true)} variant="outline" disabled={mediaLoading}>
396
- {mediaLoading ? <><Spinner className="mr-2 h-4 w-4" /> Loading...</> : "Load More"}
397
- </Button>
398
- </div>
399
- )}
400
- </div>
401
- <DialogFooter className="mt-auto pt-4 border-t">
402
- <DialogClose asChild>
403
- <Button type="button" variant="outline" onClick={() => { setMediaError(null); }}>Cancel</Button>
404
- </DialogClose>
405
- </DialogFooter>
406
- </DialogContent>
407
- </Dialog>
408
- </div>
409
- </div>
413
+ <FeatureImageField
414
+ initialImageId={initialFeatureImageId || post?.feature_image_id || null}
415
+ initialImageUrl={initialFeatureImageUrl || null}
416
+ onImageIdChange={setFeatureImageId}
417
+ uploadFolder={`posts/${(slug || 'untitled').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '')}/`}
418
+ />
410
419
 
411
- <div className="flex justify-end space-x-3 pt-6"> {/* Increased pt for spacing */}
412
- <Button type="button" variant="outline" onClick={() => router.push("/cms/posts")} disabled={isPending}>Cancel</Button>
413
- <Button type="submit" disabled={isPending || authLoading || availableLanguages.length === 0}>
414
- {isPending ? (
415
- <>
416
- <Spinner className="mr-2 h-4 w-4" /> Saving...
417
- </>
418
- ) : (
419
- actionButtonText
420
- )}
421
- </Button>
422
- </div>
420
+ {!isEditing && (
421
+ <div className="flex justify-end space-x-3 pt-6"> {/* Increased pt for spacing */}
422
+ <Button type="button" variant="outline" onClick={() => router.push("/cms/posts")} disabled={isPending}>Cancel</Button>
423
+ <Button type="submit" disabled={isPending || authLoading || availableLanguages.length === 0}>
424
+ {isPending ? (
425
+ <>
426
+ <Spinner className="mr-2 h-4 w-4" /> Saving...
427
+ </>
428
+ ) : (
429
+ actionButtonText
430
+ )}
431
+ </Button>
432
+ </div>
433
+ )}
423
434
  </form>
424
435
  );
425
436
  }
@@ -1,7 +1,7 @@
1
1
  // app/cms/posts/new/page.tsx
2
2
  import PostForm from "../components/PostForm";
3
3
  import { createPost } from "../actions";
4
- import { getLanguages } from "@/app/cms/settings/languages/actions";
4
+ import { getLanguages } from '../../settings/languages/actions';
5
5
 
6
6
  export default async function NewPostPage() {
7
7
  const languagesResult = await getLanguages();
@@ -22,12 +22,14 @@ import {
22
22
  DropdownMenuSeparator,
23
23
  } from "@nextblock-cms/ui";
24
24
  // deletePost server action is now used by DeletePostButtonClient
25
- import type { Database } from "@nextblock-cms/db";
26
- import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
27
-
28
- type Post = Database['public']['Tables']['posts']['Row'] & { feature_image_url?: string | null };
29
- import LanguageFilterSelect from "@/app/cms/components/LanguageFilterSelect";
30
- import DeletePostButtonClient from "./components/DeletePostButtonClient"; // Import the new client component
25
+ import type { Database } from "@nextblock-cms/db";
26
+ import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
27
+ import { resolveMediaUrl } from "../../../lib/media/resolveMediaUrl";
28
+
29
+ type Post = Database['public']['Tables']['posts']['Row'] & { feature_image_url?: string | null };
30
+ import LanguageFilterSelect from "../components/LanguageFilterSelect";
31
+ import DeletePostButtonClient from "./components/DeletePostButtonClient"; // Import the new client component
32
+ import { ContentTransferControls } from "../import-export/ContentTransferControls";
31
33
 
32
34
  async function getPostsWithDetails(filterLanguageId?: number): Promise<{ post: Post; languageCode: string }[]> {
33
35
  const supabase = createClient();
@@ -54,7 +56,7 @@ async function getPostsWithDetails(filterLanguageId?: number): Promise<{ post: P
54
56
  return postsData.map(p => {
55
57
  const langInfo = p.languages as unknown as { code: string } | null;
56
58
  return {
57
- post: { ...p, feature_image_url: p.media?.object_key ? `${process.env.NEXT_PUBLIC_R2_BASE_URL}/${p.media.object_key}` : null } as Post,
59
+ post: { ...p, feature_image_url: resolveMediaUrl(p.media?.object_key) } as Post,
58
60
  languageCode: langInfo?.code?.toUpperCase() || langMap.get(p.language_id)?.toUpperCase() || 'N/A',
59
61
  };
60
62
  });
@@ -80,11 +82,16 @@ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
80
82
  return (
81
83
  <div className="w-full">
82
84
  <div className="flex justify-between items-center mb-6 flex-wrap gap-4">
83
- <h1 className="text-2xl font-semibold">Manage Posts</h1>
84
- <div className="flex items-center gap-3">
85
- <LanguageFilterSelect
86
- allLanguages={allLanguages}
87
- currentFilterLangId={filterLangId}
85
+ <h1 className="text-2xl font-semibold">Manage Posts</h1>
86
+ <div className="flex items-center gap-3">
87
+ <ContentTransferControls
88
+ contentType="posts"
89
+ label="Posts"
90
+ languageId={filterLangId}
91
+ />
92
+ <LanguageFilterSelect
93
+ allLanguages={allLanguages}
94
+ currentFilterLangId={filterLangId}
88
95
  basePath="/cms/posts"
89
96
  />
90
97
  <Button variant="default" asChild>
@@ -168,7 +175,7 @@ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
168
175
  <TableCell className="text-right">
169
176
  <DropdownMenu>
170
177
  <DropdownMenuTrigger asChild>
171
- <Button variant="ghost" size="icon">
178
+ <Button id={`post-trigger-${post.id}`} variant="ghost" size="icon">
172
179
  <MoreHorizontal className="h-4 w-4" />
173
180
  <span className="sr-only">Post actions for {post.title}</span>
174
181
  </Button>