create-nextblock 0.16.1 → 0.16.2

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 (258) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -221
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -3092
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  42. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  43. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  44. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
  45. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  46. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  47. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  48. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  49. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  50. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  51. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  52. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  53. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  54. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  55. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  57. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  58. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  59. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  60. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  61. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  62. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  63. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  64. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  65. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  66. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  67. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  68. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  69. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  70. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
  71. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
  72. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  73. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  74. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  75. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  76. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  77. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  78. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  79. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  80. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  81. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  82. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  83. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  84. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  85. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  86. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  87. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  88. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  89. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  90. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  91. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  92. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  93. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  94. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  95. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  96. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  97. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  98. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  99. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  100. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  101. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  102. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  103. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  104. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  105. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  106. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  107. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  108. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  109. package/templates/nextblock-template/app/layout.tsx +671 -671
  110. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  111. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  112. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  113. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  114. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  115. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  116. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  117. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  118. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  119. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  120. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  121. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  122. package/templates/nextblock-template/app/providers.tsx +96 -96
  123. package/templates/nextblock-template/app/robots.ts +123 -123
  124. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  125. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  126. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  127. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  128. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  129. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  130. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  131. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  132. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  133. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  134. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  135. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  136. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  137. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  138. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  139. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  140. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  141. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  142. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  143. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  144. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  145. package/templates/nextblock-template/components/Header.tsx +38 -38
  146. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  147. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  148. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  149. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  150. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  151. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  152. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  153. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  154. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  155. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  156. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  157. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  158. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  159. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  160. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  161. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  162. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  163. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  164. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  165. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  166. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  167. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  168. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  169. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  170. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  171. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  172. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  173. package/templates/nextblock-template/components/form-message.tsx +32 -32
  174. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
  175. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
  176. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  177. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  178. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  179. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  180. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  181. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  182. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  183. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  184. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  185. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  186. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  187. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  188. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  189. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  190. package/templates/nextblock-template/docs/README.md +42 -42
  191. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  192. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  193. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  194. package/templates/nextblock-template/index.d.ts +7 -7
  195. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  196. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  197. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  198. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  199. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  200. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  201. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  202. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  203. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  204. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  205. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  206. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  207. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
  208. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  209. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  210. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  211. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  212. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  213. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  214. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  215. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  216. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  217. package/templates/nextblock-template/lib/search/types.ts +27 -27
  218. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
  219. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  220. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  221. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  222. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  223. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
  224. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
  225. package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
  226. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  227. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  228. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  229. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  230. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  231. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  232. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
  233. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  234. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  235. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  236. package/templates/nextblock-template/lib/site-url.ts +102 -102
  237. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  238. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  239. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  240. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  241. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  242. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  243. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  244. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  245. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  246. package/templates/nextblock-template/next-env.d.ts +1 -2
  247. package/templates/nextblock-template/package.json +1 -1
  248. package/templates/nextblock-template/postcss.config.js +6 -6
  249. package/templates/nextblock-template/scripts/backup.js +115 -115
  250. package/templates/nextblock-template/scripts/restore.js +385 -385
  251. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  252. package/templates/nextblock-template/tailwind.config.js +25 -25
  253. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  254. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  255. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  256. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  257. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  258. package/tsconfig.base.json +3 -3
@@ -1,93 +1,93 @@
1
- /**
2
- * The snake_case ↔ camelCase boundary for managed redirects.
3
- *
4
- * The database speaks `source_path` / `is_active`; the pure SEO engine in
5
- * `@nextblock-cms/utils/seo` speaks `sourcePath` / `isActive`. Exactly one place is
6
- * allowed to know both vocabularies, and this is it — every other module on this
7
- * screen sees one shape or the other and never has to guess which.
8
- *
9
- * These helpers are pure and live outside `actions.ts` deliberately: that file is
10
- * `'use server'`, where every export must be an async server action, so a synchronous
11
- * mapper cannot be exported from there. Keeping them here also means the client
12
- * components can use the identical mapping the actions use, which is what lets the
13
- * browser run the real `validateRedirectRule` against the real rule set before a
14
- * request is ever made.
15
- */
16
-
17
- import type { RedirectRule, RedirectStatusCode } from '@nextblock-cms/utils/seo';
18
-
19
- import { mapRedirectRow, type CmsRedirectRow } from '../../../../lib/seo/redirect-store';
20
-
21
- /**
22
- * A row of `public.cms_redirects` as this screen sees it.
23
- *
24
- * WHY IT IS AN ALIAS AND NOT A SECOND HAND-WRITTEN INTERFACE. The row shape was
25
- * already transcribed from migration 00000000000030's DDL in `lib/seo/redirect-store.ts`,
26
- * for the proxy's benefit, and for the same reason it had to be hand-written there:
27
- * `libs/db`'s generated `Database` type comes from `npm run db:types` against a live
28
- * project, that migration has not been applied yet, and so
29
- * `Database['public']['Tables']['cms_redirects']` does not compile today. Restating
30
- * the seven columns here would give the repo two hand-written descriptions of one
31
- * table that could drift apart in a rename; aliasing gives it one. When the migration
32
- * lands and the types are regenerated, there is a single interface to delete.
33
- */
34
- export type CmsRedirect = CmsRedirectRow;
35
-
36
- /**
37
- * What the add/edit form collects, in the engine's vocabulary.
38
- *
39
- * `statusCode` is `RedirectStatusCode` rather than `number` so a widened value cannot
40
- * reach the database and trip the table's CHECK constraint as an opaque Postgres
41
- * error; the server re-validates it with Zod regardless, because a type says nothing
42
- * about what actually arrived over the wire.
43
- */
44
- export interface RedirectInput {
45
- destinationPath: string;
46
- isActive: boolean;
47
- sourcePath: string;
48
- statusCode: RedirectStatusCode;
49
- }
50
-
51
- /**
52
- * Maps stored rows onto the engine's rule shape, dropping any row too malformed to
53
- * be a rule at all.
54
- *
55
- * Reuses `mapRedirectRow`, the proxy's mapper, so the admin screen's idea of the
56
- * current rule set is byte-for-byte the proxy's idea of it. That agreement is what
57
- * makes client-side validation trustworthy: a duplicate or a loop the admin screen
58
- * refuses is a duplicate or a loop the proxy would actually have hit.
59
- *
60
- * Unlike the proxy's query this one is given inactive rows too, and that is
61
- * intentional — `validateRedirectRule` counts a paused rule when checking for
62
- * duplicates and loops, so re-enabling one cannot introduce a conflict that
63
- * validation already waved through.
64
- */
65
- export function toRedirectRules(rows: CmsRedirect[]): RedirectRule[] {
66
- const rules: RedirectRule[] = [];
67
-
68
- for (const row of rows) {
69
- const rule = mapRedirectRow(row);
70
- if (rule !== null) {
71
- rules.push(rule);
72
- }
73
- }
74
-
75
- return rules;
76
- }
77
-
78
- /**
79
- * Maps one stored row onto the form's input shape, for the inline editor.
80
- *
81
- * Falls back to 301 for an out-of-range `status_code` for the same reason
82
- * `mapRedirectRow` does: the column is constrained to 301/302, so the fallback never
83
- * fires in practice, and defaulting to permanent keeps a rule the operator can see in
84
- * the list from silently becoming un-editable.
85
- */
86
- export function toRedirectInput(row: CmsRedirect): RedirectInput {
87
- return {
88
- destinationPath: row.destination_path,
89
- isActive: row.is_active,
90
- sourcePath: row.source_path,
91
- statusCode: row.status_code === 302 ? 302 : 301,
92
- };
93
- }
1
+ /**
2
+ * The snake_case ↔ camelCase boundary for managed redirects.
3
+ *
4
+ * The database speaks `source_path` / `is_active`; the pure SEO engine in
5
+ * `@nextblock-cms/utils/seo` speaks `sourcePath` / `isActive`. Exactly one place is
6
+ * allowed to know both vocabularies, and this is it — every other module on this
7
+ * screen sees one shape or the other and never has to guess which.
8
+ *
9
+ * These helpers are pure and live outside `actions.ts` deliberately: that file is
10
+ * `'use server'`, where every export must be an async server action, so a synchronous
11
+ * mapper cannot be exported from there. Keeping them here also means the client
12
+ * components can use the identical mapping the actions use, which is what lets the
13
+ * browser run the real `validateRedirectRule` against the real rule set before a
14
+ * request is ever made.
15
+ */
16
+
17
+ import type { RedirectRule, RedirectStatusCode } from '@nextblock-cms/utils/seo';
18
+
19
+ import { mapRedirectRow, type CmsRedirectRow } from '../../../../lib/seo/redirect-store';
20
+
21
+ /**
22
+ * A row of `public.cms_redirects` as this screen sees it.
23
+ *
24
+ * WHY IT IS AN ALIAS AND NOT A SECOND HAND-WRITTEN INTERFACE. The row shape was
25
+ * already transcribed from migration 00000000000030's DDL in `lib/seo/redirect-store.ts`,
26
+ * for the proxy's benefit, and for the same reason it had to be hand-written there:
27
+ * `libs/db`'s generated `Database` type comes from `npm run db:types` against a live
28
+ * project, that migration has not been applied yet, and so
29
+ * `Database['public']['Tables']['cms_redirects']` does not compile today. Restating
30
+ * the seven columns here would give the repo two hand-written descriptions of one
31
+ * table that could drift apart in a rename; aliasing gives it one. When the migration
32
+ * lands and the types are regenerated, there is a single interface to delete.
33
+ */
34
+ export type CmsRedirect = CmsRedirectRow;
35
+
36
+ /**
37
+ * What the add/edit form collects, in the engine's vocabulary.
38
+ *
39
+ * `statusCode` is `RedirectStatusCode` rather than `number` so a widened value cannot
40
+ * reach the database and trip the table's CHECK constraint as an opaque Postgres
41
+ * error; the server re-validates it with Zod regardless, because a type says nothing
42
+ * about what actually arrived over the wire.
43
+ */
44
+ export interface RedirectInput {
45
+ destinationPath: string;
46
+ isActive: boolean;
47
+ sourcePath: string;
48
+ statusCode: RedirectStatusCode;
49
+ }
50
+
51
+ /**
52
+ * Maps stored rows onto the engine's rule shape, dropping any row too malformed to
53
+ * be a rule at all.
54
+ *
55
+ * Reuses `mapRedirectRow`, the proxy's mapper, so the admin screen's idea of the
56
+ * current rule set is byte-for-byte the proxy's idea of it. That agreement is what
57
+ * makes client-side validation trustworthy: a duplicate or a loop the admin screen
58
+ * refuses is a duplicate or a loop the proxy would actually have hit.
59
+ *
60
+ * Unlike the proxy's query this one is given inactive rows too, and that is
61
+ * intentional — `validateRedirectRule` counts a paused rule when checking for
62
+ * duplicates and loops, so re-enabling one cannot introduce a conflict that
63
+ * validation already waved through.
64
+ */
65
+ export function toRedirectRules(rows: CmsRedirect[]): RedirectRule[] {
66
+ const rules: RedirectRule[] = [];
67
+
68
+ for (const row of rows) {
69
+ const rule = mapRedirectRow(row);
70
+ if (rule !== null) {
71
+ rules.push(rule);
72
+ }
73
+ }
74
+
75
+ return rules;
76
+ }
77
+
78
+ /**
79
+ * Maps one stored row onto the form's input shape, for the inline editor.
80
+ *
81
+ * Falls back to 301 for an out-of-range `status_code` for the same reason
82
+ * `mapRedirectRow` does: the column is constrained to 301/302, so the fallback never
83
+ * fires in practice, and defaulting to permanent keeps a rule the operator can see in
84
+ * the list from silently becoming un-editable.
85
+ */
86
+ export function toRedirectInput(row: CmsRedirect): RedirectInput {
87
+ return {
88
+ destinationPath: row.destination_path,
89
+ isActive: row.is_active,
90
+ sourcePath: row.source_path,
91
+ statusCode: row.status_code === 302 ? 302 : 301,
92
+ };
93
+ }
@@ -1,46 +1,46 @@
1
- // app/cms/settings/seo/page.tsx
2
- import { hasResolvedSiteUrl, resolveSiteUrl } from '../../../../lib/site-url';
3
- import { getRedirects, getRobotsSettings } from './actions';
4
- import { requireAdminSupabaseClient } from './require-admin';
5
- import { SeoSettingsClient } from './SeoSettingsClient';
6
-
7
- /**
8
- * The SEO operations screen: which URLs moved, and what crawlers are allowed to do.
9
- *
10
- * ADMIN-only, and gated here as well as in every action and in RLS. The gate is
11
- * repeated rather than delegated because the three layers protect different things:
12
- * this one stops a WRITER from ever seeing the screen (a redirect list is a map of a
13
- * site's history, and the robots block is a map of what it is hiding), the actions
14
- * stop a hand-rolled request that never loaded the page, and RLS stops everything
15
- * else. `requireAdminSupabaseClient` throws, which is the right behaviour for a
16
- * Server Component: an unauthorised visitor gets the error boundary instead of a
17
- * half-rendered admin screen.
18
- *
19
- * The two reads run concurrently because they are independent — one hits
20
- * `cms_redirects`, the other one row of `site_settings` — and running them in series
21
- * would add a whole round trip to a screen that is already behind an auth check.
22
- */
23
- export default async function SeoSettingsPage() {
24
- await requireAdminSupabaseClient();
25
-
26
- const [redirects, robotsSettings] = await Promise.all([getRedirects(), getRobotsSettings()]);
27
-
28
- // Resolved on the server, where the deployment's env actually lives, and handed to
29
- // the preview as data. The client component must never try to work this out for
30
- // itself: `NEXT_PUBLIC_URL` is inlined at build time and the Vercel fallbacks are
31
- // server-only, so a browser-side guess would quietly show a different sitemap URL
32
- // from the one crawlers are given.
33
- const siteUrl = resolveSiteUrl();
34
-
35
- return (
36
- <div className="mx-auto max-w-5xl space-y-6">
37
- <SeoSettingsClient
38
- initialRedirects={redirects}
39
- initialRobotsSettings={robotsSettings}
40
- isSandbox={process.env.NEXT_PUBLIC_IS_SANDBOX === 'true'}
41
- isSiteUrlConfigured={hasResolvedSiteUrl()}
42
- sitemapUrl={`${siteUrl}/sitemap.xml`}
43
- />
44
- </div>
45
- );
46
- }
1
+ // app/cms/settings/seo/page.tsx
2
+ import { hasResolvedSiteUrl, resolveSiteUrl } from '../../../../lib/site-url';
3
+ import { getRedirects, getRobotsSettings } from './actions';
4
+ import { requireAdminSupabaseClient } from './require-admin';
5
+ import { SeoSettingsClient } from './SeoSettingsClient';
6
+
7
+ /**
8
+ * The SEO operations screen: which URLs moved, and what crawlers are allowed to do.
9
+ *
10
+ * ADMIN-only, and gated here as well as in every action and in RLS. The gate is
11
+ * repeated rather than delegated because the three layers protect different things:
12
+ * this one stops a WRITER from ever seeing the screen (a redirect list is a map of a
13
+ * site's history, and the robots block is a map of what it is hiding), the actions
14
+ * stop a hand-rolled request that never loaded the page, and RLS stops everything
15
+ * else. `requireAdminSupabaseClient` throws, which is the right behaviour for a
16
+ * Server Component: an unauthorised visitor gets the error boundary instead of a
17
+ * half-rendered admin screen.
18
+ *
19
+ * The two reads run concurrently because they are independent — one hits
20
+ * `cms_redirects`, the other one row of `site_settings` — and running them in series
21
+ * would add a whole round trip to a screen that is already behind an auth check.
22
+ */
23
+ export default async function SeoSettingsPage() {
24
+ await requireAdminSupabaseClient();
25
+
26
+ const [redirects, robotsSettings] = await Promise.all([getRedirects(), getRobotsSettings()]);
27
+
28
+ // Resolved on the server, where the deployment's env actually lives, and handed to
29
+ // the preview as data. The client component must never try to work this out for
30
+ // itself: `NEXT_PUBLIC_URL` is inlined at build time and the Vercel fallbacks are
31
+ // server-only, so a browser-side guess would quietly show a different sitemap URL
32
+ // from the one crawlers are given.
33
+ const siteUrl = resolveSiteUrl();
34
+
35
+ return (
36
+ <div className="mx-auto max-w-5xl space-y-6">
37
+ <SeoSettingsClient
38
+ initialRedirects={redirects}
39
+ initialRobotsSettings={robotsSettings}
40
+ isSandbox={process.env.NEXT_PUBLIC_IS_SANDBOX === 'true'}
41
+ isSiteUrlConfigured={hasResolvedSiteUrl()}
42
+ sitemapUrl={`${siteUrl}/sitemap.xml`}
43
+ />
44
+ </div>
45
+ );
46
+ }
@@ -1,47 +1,47 @@
1
- import { createClient } from '@nextblock-cms/db/server';
2
-
3
- /**
4
- * Admin gate shared by the SEO settings server actions and by the page itself.
5
- *
6
- * Lives outside `actions.ts` because that file is `'use server'`, where every export
7
- * must itself be a valid server action — a helper that returns a Supabase client
8
- * cannot be exported from there. Keeping one copy matters more than the file count:
9
- * this is the check standing between a WRITER and the redirect table, and a redirect
10
- * rule is an open-redirect surface. Anyone who can write one can point every visitor
11
- * of `/checkout` at a phishing clone of the site, with the real domain in the
12
- * referrer, without touching a single page or post. The same reasoning is why
13
- * migration 00000000000030 grants writes on `cms_redirects` to ADMIN only and leaves
14
- * WRITER out entirely — the RLS policy is the boundary, and this is the friendly
15
- * error that keeps an operator from meeting it as an unexplained empty list.
16
- *
17
- * It throws rather than returning a result because its two kinds of caller want
18
- * opposite things. A Server Component wants the throw: an unauthorised visitor gets
19
- * the error boundary instead of a half-rendered admin screen. A server action called
20
- * from a client component wants a message it can render, so those callers catch the
21
- * throw and return it as data — see `requireAdminOrMessage` in `actions.ts` and the
22
- * note on `SettingsActionResult` about Next replacing uncaught Server Action error
23
- * messages with a generic string in production.
24
- */
25
- export async function requireAdminSupabaseClient() {
26
- const supabase = createClient();
27
- const {
28
- data: { user },
29
- error: userError,
30
- } = await supabase.auth.getUser();
31
-
32
- if (userError || !user) {
33
- throw new Error('You must be logged in to manage SEO settings.');
34
- }
35
-
36
- const { data: profile, error: profileError } = await supabase
37
- .from('profiles')
38
- .select('role')
39
- .eq('id', user.id)
40
- .single();
41
-
42
- if (profileError || !profile || profile.role !== 'ADMIN') {
43
- throw new Error('You do not have permission to manage SEO settings.');
44
- }
45
-
46
- return { supabase, userId: user.id };
47
- }
1
+ import { createClient } from '@nextblock-cms/db/server';
2
+
3
+ /**
4
+ * Admin gate shared by the SEO settings server actions and by the page itself.
5
+ *
6
+ * Lives outside `actions.ts` because that file is `'use server'`, where every export
7
+ * must itself be a valid server action — a helper that returns a Supabase client
8
+ * cannot be exported from there. Keeping one copy matters more than the file count:
9
+ * this is the check standing between a WRITER and the redirect table, and a redirect
10
+ * rule is an open-redirect surface. Anyone who can write one can point every visitor
11
+ * of `/checkout` at a phishing clone of the site, with the real domain in the
12
+ * referrer, without touching a single page or post. The same reasoning is why
13
+ * migration 00000000000030 grants writes on `cms_redirects` to ADMIN only and leaves
14
+ * WRITER out entirely — the RLS policy is the boundary, and this is the friendly
15
+ * error that keeps an operator from meeting it as an unexplained empty list.
16
+ *
17
+ * It throws rather than returning a result because its two kinds of caller want
18
+ * opposite things. A Server Component wants the throw: an unauthorised visitor gets
19
+ * the error boundary instead of a half-rendered admin screen. A server action called
20
+ * from a client component wants a message it can render, so those callers catch the
21
+ * throw and return it as data — see `requireAdminOrMessage` in `actions.ts` and the
22
+ * note on `SettingsActionResult` about Next replacing uncaught Server Action error
23
+ * messages with a generic string in production.
24
+ */
25
+ export async function requireAdminSupabaseClient() {
26
+ const supabase = createClient();
27
+ const {
28
+ data: { user },
29
+ error: userError,
30
+ } = await supabase.auth.getUser();
31
+
32
+ if (userError || !user) {
33
+ throw new Error('You must be logged in to manage SEO settings.');
34
+ }
35
+
36
+ const { data: profile, error: profileError } = await supabase
37
+ .from('profiles')
38
+ .select('role')
39
+ .eq('id', user.id)
40
+ .single();
41
+
42
+ if (profileError || !profile || profile.role !== 'ADMIN') {
43
+ throw new Error('You do not have permission to manage SEO settings.');
44
+ }
45
+
46
+ return { supabase, userId: user.id };
47
+ }
@@ -1,51 +1,51 @@
1
- // app/cms/settings/site-scripts/page.tsx
2
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextblock-cms/ui';
3
-
4
- import { getSiteScriptRevisions, getSiteScripts } from './actions';
5
- import SiteScriptManager from './components/SiteScriptManager';
6
-
7
- export default async function SiteScriptsSettingsPage() {
8
- const [scripts, revisions] = await Promise.all([getSiteScripts(), getSiteScriptRevisions()]);
9
-
10
- return (
11
- <div className="mx-auto max-w-5xl space-y-6">
12
- <Card>
13
- <CardHeader>
14
- <CardTitle>Site Scripts</CardTitle>
15
- <CardDescription>
16
- JavaScript that runs on every page of the public site — animation helpers, chat widgets,
17
- third-party embeds. Each script is injected with the page&apos;s Content-Security-Policy
18
- nonce, so it runs without weakening the site&apos;s security headers. Disabled scripts are
19
- never sent to visitors. Every change is logged and can be rolled back from the history
20
- below.
21
- </CardDescription>
22
- </CardHeader>
23
- <CardContent>
24
- <SiteScriptManager initialRevisions={revisions} initialScripts={scripts} />
25
- </CardContent>
26
- </Card>
27
-
28
- <Card>
29
- <CardHeader>
30
- <CardTitle>Where else code can live</CardTitle>
31
- </CardHeader>
32
- <CardContent className="space-y-2 text-sm text-muted-foreground">
33
- <p>
34
- <strong className="text-foreground">One page only?</strong> A rich-text block accepts an
35
- inline <code>&lt;style&gt;</code> and <code>&lt;script&gt;</code> directly, so
36
- page-specific effects do not need to live here.
37
- </p>
38
- <p>
39
- <strong className="text-foreground">Site-wide styling?</strong> Use Themes &amp; CSS for
40
- palettes and global stylesheets.
41
- </p>
42
- <p>
43
- <strong className="text-foreground">Marketing or analytics tags?</strong> Put those under
44
- Google Analytics instead — scripts there only fire once a visitor accepts cookies, which
45
- is what consent law requires. Scripts on this page run unconditionally.
46
- </p>
47
- </CardContent>
48
- </Card>
49
- </div>
50
- );
51
- }
1
+ // app/cms/settings/site-scripts/page.tsx
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextblock-cms/ui';
3
+
4
+ import { getSiteScriptRevisions, getSiteScripts } from './actions';
5
+ import SiteScriptManager from './components/SiteScriptManager';
6
+
7
+ export default async function SiteScriptsSettingsPage() {
8
+ const [scripts, revisions] = await Promise.all([getSiteScripts(), getSiteScriptRevisions()]);
9
+
10
+ return (
11
+ <div className="mx-auto max-w-5xl space-y-6">
12
+ <Card>
13
+ <CardHeader>
14
+ <CardTitle>Site Scripts</CardTitle>
15
+ <CardDescription>
16
+ JavaScript that runs on every page of the public site — animation helpers, chat widgets,
17
+ third-party embeds. Each script is injected with the page&apos;s Content-Security-Policy
18
+ nonce, so it runs without weakening the site&apos;s security headers. Disabled scripts are
19
+ never sent to visitors. Every change is logged and can be rolled back from the history
20
+ below.
21
+ </CardDescription>
22
+ </CardHeader>
23
+ <CardContent>
24
+ <SiteScriptManager initialRevisions={revisions} initialScripts={scripts} />
25
+ </CardContent>
26
+ </Card>
27
+
28
+ <Card>
29
+ <CardHeader>
30
+ <CardTitle>Where else code can live</CardTitle>
31
+ </CardHeader>
32
+ <CardContent className="space-y-2 text-sm text-muted-foreground">
33
+ <p>
34
+ <strong className="text-foreground">One page only?</strong> A rich-text block accepts an
35
+ inline <code>&lt;style&gt;</code> and <code>&lt;script&gt;</code> directly, so
36
+ page-specific effects do not need to live here.
37
+ </p>
38
+ <p>
39
+ <strong className="text-foreground">Site-wide styling?</strong> Use Themes &amp; CSS for
40
+ palettes and global stylesheets.
41
+ </p>
42
+ <p>
43
+ <strong className="text-foreground">Marketing or analytics tags?</strong> Put those under
44
+ Google Analytics instead — scripts there only fire once a visitor accepts cookies, which
45
+ is what consent law requires. Scripts on this page run unconditionally.
46
+ </p>
47
+ </CardContent>
48
+ </Card>
49
+ </div>
50
+ );
51
+ }
@@ -1,21 +1,21 @@
1
- import { redirect } from 'next/navigation';
2
- import { verifyPackageOnline } from '@nextblock-cms/db/server';
3
- import { TaxesPage as TaxesPageUI } from '@nextblock-cms/ecommerce/server';
4
-
5
- export const metadata = {
6
- title: 'Tax Settings | NextBlock™ CMS',
7
- };
8
-
9
- export default async function TaxesPageWrapper({
10
- searchParams,
11
- }: {
12
- searchParams?: Promise<{ success?: string }>;
13
- }) {
14
- const isOnline = await verifyPackageOnline('ecommerce');
15
-
16
- if (!isOnline) {
17
- redirect('/cms/settings/packages');
18
- }
19
-
20
- return <TaxesPageUI searchParams={await searchParams} />;
21
- }
1
+ import { redirect } from 'next/navigation';
2
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
3
+ import { TaxesPage as TaxesPageUI } from '@nextblock-cms/ecommerce/server';
4
+
5
+ export const metadata = {
6
+ title: 'Tax Settings | NextBlock™ CMS',
7
+ };
8
+
9
+ export default async function TaxesPageWrapper({
10
+ searchParams,
11
+ }: {
12
+ searchParams?: Promise<{ success?: string }>;
13
+ }) {
14
+ const isOnline = await verifyPackageOnline('ecommerce');
15
+
16
+ if (!isOnline) {
17
+ redirect('/cms/settings/packages');
18
+ }
19
+
20
+ return <TaxesPageUI searchParams={await searchParams} />;
21
+ }
@@ -1,20 +1,20 @@
1
- import { redirect } from 'next/navigation';
2
- import { verifyPackageOnline } from '@nextblock-cms/db/server';
3
- import { ShippingPage as ShippingPageUI } from '@nextblock-cms/ecommerce/server';
4
-
5
- export default async function ShippingPage({
6
- searchParams,
7
- }: {
8
- searchParams: Promise<{ success?: string }>;
9
- }) {
10
- const [isOnline, resolvedSearchParams] = await Promise.all([
11
- verifyPackageOnline('ecommerce'),
12
- searchParams,
13
- ]);
14
-
15
- if (!isOnline) {
16
- redirect('/cms/settings/packages');
17
- }
18
-
19
- return <ShippingPageUI searchParams={resolvedSearchParams} />;
20
- }
1
+ import { redirect } from 'next/navigation';
2
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
3
+ import { ShippingPage as ShippingPageUI } from '@nextblock-cms/ecommerce/server';
4
+
5
+ export default async function ShippingPage({
6
+ searchParams,
7
+ }: {
8
+ searchParams: Promise<{ success?: string }>;
9
+ }) {
10
+ const [isOnline, resolvedSearchParams] = await Promise.all([
11
+ verifyPackageOnline('ecommerce'),
12
+ searchParams,
13
+ ]);
14
+
15
+ if (!isOnline) {
16
+ redirect('/cms/settings/packages');
17
+ }
18
+
19
+ return <ShippingPageUI searchParams={resolvedSearchParams} />;
20
+ }
@@ -7,17 +7,17 @@ import { Trash2, ShieldAlert } from "lucide-react";
7
7
  import { deleteUserAndProfile } from "../actions";
8
8
  import { ConfirmationModal } from '../../components/ConfirmationModal';
9
9
 
10
- export function DeleteUserButtonClient({
11
- userId,
12
- userEmail,
13
- currentAdminId,
14
- }: {
15
- userId: string;
16
- userEmail?: string;
17
- currentAdminId?: string;
18
- }) {
19
- void userEmail;
20
- const [isPending, startTransition] = useTransition();
10
+ export function DeleteUserButtonClient({
11
+ userId,
12
+ userEmail,
13
+ currentAdminId,
14
+ }: {
15
+ userId: string;
16
+ userEmail?: string;
17
+ currentAdminId?: string;
18
+ }) {
19
+ void userEmail;
20
+ const [isPending, startTransition] = useTransition();
21
21
  const [isModalOpen, setIsModalOpen] = useState(false);
22
22
 
23
23
  const handleDelete = () => {
@@ -68,4 +68,4 @@ export function DeleteUserButtonClient({
68
68
  />
69
69
  </>
70
70
  );
71
- }
71
+ }