create-nextblock 0.14.0 → 0.14.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 (242) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  19. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  20. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  21. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  22. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  23. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  24. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  25. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  26. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  27. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  28. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  29. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  30. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  31. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  32. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5749
  33. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  34. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  35. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  36. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  37. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  38. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  39. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  40. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  41. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  42. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  43. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  44. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  45. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  46. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +4 -2
  47. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  48. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  51. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  52. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  53. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  54. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  55. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  56. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  57. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  58. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  59. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  60. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  62. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  63. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  64. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  65. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  66. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  67. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  68. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  69. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  70. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  71. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  72. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  73. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  74. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  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/promotions/PromotionsWorkspace.tsx +456 -456
  78. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  79. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  80. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  81. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  82. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  83. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  84. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  85. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +688 -688
  86. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  87. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  88. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  89. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  90. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  91. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  92. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  93. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  94. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  95. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +23 -21
  96. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  97. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  98. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  99. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  100. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  101. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  102. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +9 -7
  103. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  104. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  105. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  106. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  107. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  108. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  109. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  110. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  111. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  112. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  113. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  114. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  115. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  116. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  117. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  118. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  119. package/templates/nextblock-template/app/sitemap.ts +130 -130
  120. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  121. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  122. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  123. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  124. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  125. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  126. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  127. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  128. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  129. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  130. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  131. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  132. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  133. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  134. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  135. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  136. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  137. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  138. package/templates/nextblock-template/components/form-message.tsx +32 -32
  139. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  140. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  141. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  142. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  143. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  144. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  145. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  146. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  147. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  148. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  149. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  150. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  151. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  152. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  153. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  154. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  155. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  156. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  157. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  158. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  159. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  160. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  161. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  162. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  163. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  164. package/templates/nextblock-template/docs/README.md +39 -39
  165. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  166. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  167. package/templates/nextblock-template/index.d.ts +7 -7
  168. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  169. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  170. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  171. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  172. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  173. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  174. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  175. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  176. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  177. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  178. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  179. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  180. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  181. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  182. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  183. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  184. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  185. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  186. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  187. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  188. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  189. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  190. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  191. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  192. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  193. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  194. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  195. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  196. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  197. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  198. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  199. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  200. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  201. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  202. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  203. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  204. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  205. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  206. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  207. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  208. package/templates/nextblock-template/lib/search/types.ts +27 -27
  209. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  210. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  211. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  212. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -87
  213. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  214. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  215. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  216. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  217. package/templates/nextblock-template/lib/site-url.ts +48 -48
  218. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  219. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  220. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  221. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  222. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  223. package/templates/nextblock-template/next-env.d.ts +1 -0
  224. package/templates/nextblock-template/package.json +6 -6
  225. package/templates/nextblock-template/postcss.config.js +6 -6
  226. package/templates/nextblock-template/scripts/backup.js +115 -115
  227. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  228. package/templates/nextblock-template/scripts/restore.js +385 -385
  229. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  230. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  231. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  232. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  233. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  234. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  235. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  236. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  237. package/templates/nextblock-template/tailwind.config.js +25 -25
  238. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  239. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  240. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  241. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  242. package/tsconfig.base.json +3 -3
@@ -1,222 +1,222 @@
1
- import 'server-only';
2
- // Derives the dashboard onboarding checklist live from configuration state. Every check
3
- // reads public/service-role data only, so it works for both ADMIN and WRITER dashboards.
4
- import { createClient } from '@nextblock-cms/db/server';
5
- import { getStoreConfigStatus } from '@nextblock-cms/ecommerce/server';
6
- import { getEmailPublicSettings } from '../config/email-settings';
7
- import { getPrivacySettings } from '../privacy/settings';
8
- import { detectChannel } from '../setup/env-status';
9
- import { getSystemConfiguration } from '../setup/system-config';
10
- import { selfActionsSettingsUrl } from '../updates/repo-identity';
11
- import { isGithubConnectAvailable } from '../updates/github-device';
12
-
13
- export type OnboardingStep = {
14
- key: string;
15
- title: string;
16
- description: string;
17
- href: string;
18
- done: boolean;
19
- optional: boolean;
20
- /** When true, render the CTA as an external link (new tab) instead of an in-app route. */
21
- isExternal?: boolean;
22
- /** When true, render the device-flow "Connect GitHub" control instead of a link. */
23
- connectGithub?: boolean;
24
- };
25
-
26
- export type OnboardingStatus = {
27
- steps: OnboardingStep[];
28
- completed: number;
29
- total: number;
30
- dismissed: boolean;
31
- };
32
-
33
- // Seeded defaults (libs/db baseline seed 00000000000003 + migration 00000000000004). The
34
- // branding/copyright steps count as "done" only when the value has been customized away from
35
- // these — a fresh install ships with the seeds, so a plain presence check would mark every
36
- // step complete immediately.
37
- const SEEDED_SITE_TITLE = 'NextBlock™ CMS';
38
- // Every object_key a fresh install may ship as the DEFAULT logo — the original baseline WebP
39
- // and the migration-00000000000004 email-safe PNG (which repoints the seeded default). A logo
40
- // counts as customized only when it is none of these; a new default swap must be added here or
41
- // branding falsely reads as done out of the box. Mirrors BUNDLED_PUBLIC_MEDIA_KEYS in
42
- // lib/media/resolveMediaUrl.ts.
43
- const SEEDED_LOGO_OBJECT_KEYS = new Set<string>([
44
- 'images/nextblock-logo-small.webp',
45
- 'images/nextblock-logo-button-tiny.png',
46
- ]);
47
- const SEEDED_COPYRIGHT: Record<string, string> = {
48
- en: '© {year} Nextblock CMS. All rights reserved.',
49
- fr: '© {year} Nextblock CMS. Tous droits réservés.',
50
- };
51
-
52
- /** True when at least one language's copyright text is set AND differs from the seed. */
53
- function hasCustomCopyright(value: unknown): boolean {
54
- if (!value || typeof value !== 'object') return false;
55
- return Object.entries(value as Record<string, unknown>).some(([lang, v]) => {
56
- if (typeof v !== 'string') return false;
57
- const trimmed = v.trim();
58
- return trimmed.length > 0 && trimmed !== (SEEDED_COPYRIGHT[lang] ?? '');
59
- });
60
- }
61
-
62
- /** Pull the active logo's media object_key from the (to-one) embedded relation. */
63
- function extractLogoObjectKey(logoRow: unknown): string | null {
64
- if (!logoRow || typeof logoRow !== 'object') return null;
65
- const media = (logoRow as { media?: unknown }).media;
66
- const record = Array.isArray(media) ? media[0] : media;
67
- if (!record || typeof record !== 'object') return null;
68
- const key = (record as { object_key?: unknown }).object_key;
69
- return typeof key === 'string' ? key : null;
70
- }
71
-
72
- export async function getOnboardingStatus(opts: {
73
- isEcommerceActive: boolean;
74
- }): Promise<OnboardingStatus> {
75
- const supabase = createClient();
76
-
77
- const [{ data: settingRows }, { data: logoRow }, emailPublic, privacy] = await Promise.all([
78
- supabase
79
- .from('site_settings')
80
- .select('key, value')
81
- .in('key', ['footer_copyright', 'bot_protection_public', 'onboarding_state', 'site_title']),
82
- supabase
83
- .from('logos')
84
- .select('media:media_id(object_key)')
85
- .order('created_at', { ascending: false })
86
- .limit(1)
87
- .maybeSingle(),
88
- getEmailPublicSettings(),
89
- getPrivacySettings(),
90
- ]);
91
-
92
- const rows = new Map((settingRows ?? []).map((r) => [r.key, r.value]));
93
- const botPublic = (rows.get('bot_protection_public') ?? {}) as Record<string, unknown>;
94
- const onboardingState = (rows.get('onboarding_state') ?? {}) as Record<string, unknown>;
95
-
96
- const siteTitleRaw = rows.get('site_title');
97
- const siteTitle = typeof siteTitleRaw === 'string' ? siteTitleRaw.trim() : '';
98
- const siteTitleCustomized = siteTitle.length > 0 && siteTitle !== SEEDED_SITE_TITLE;
99
- const logoObjectKey = extractLogoObjectKey(logoRow);
100
- const logoCustomized = Boolean(logoObjectKey) && !SEEDED_LOGO_OBJECT_KEYS.has(logoObjectKey ?? '');
101
-
102
- // Branding is "done" once the user renames the site or uploads their own logo — not merely
103
- // because the seeded NextBlock title/logo exist.
104
- const brandingDone = siteTitleCustomized || logoCustomized;
105
- const copyrightDone = hasCustomCopyright(rows.get('footer_copyright'));
106
- const emailDone = Boolean(emailPublic.host) || Boolean(process.env['SMTP_HOST']);
107
- const analyticsDone = Boolean(privacy.gtm_id);
108
- const botProvider = typeof botPublic['provider'] === 'string' ? (botPublic['provider'] as string) : 'none';
109
- const botDone = botProvider !== 'none' && botProvider !== '';
110
-
111
- const steps: OnboardingStep[] = [
112
- {
113
- key: 'admin',
114
- title: 'Create your admin account',
115
- description: 'Your administrator account is set up.',
116
- href: '/cms/users',
117
- done: true,
118
- optional: false,
119
- },
120
- {
121
- key: 'branding',
122
- title: 'Add your branding',
123
- description: 'Upload your logo and set your site identity.',
124
- href: '/cms/settings/logos',
125
- done: brandingDone,
126
- optional: false,
127
- },
128
- {
129
- key: 'copyright',
130
- title: 'Set your copyright / footer',
131
- description: 'Configure the footer copyright shown across your site.',
132
- href: '/cms/settings/copyright',
133
- done: copyrightDone,
134
- optional: false,
135
- },
136
- {
137
- key: 'email',
138
- title: 'Configure email (SMTP)',
139
- description: 'Enable verification emails, password resets, and notifications.',
140
- href: '/cms/settings/email',
141
- done: emailDone,
142
- optional: false,
143
- },
144
- {
145
- key: 'analytics',
146
- title: 'Connect analytics',
147
- description: 'Add a Google Tag Manager ID for consent-gated analytics.',
148
- href: '/cms/settings/google-analytics',
149
- done: analyticsDone,
150
- optional: true,
151
- },
152
- {
153
- key: 'bot',
154
- title: 'Enable bot protection',
155
- description: 'Protect your sign-up and sign-in forms with a CAPTCHA.',
156
- href: '/cms/settings/bot-protection',
157
- done: botDone,
158
- optional: true,
159
- },
160
- ];
161
-
162
- if (opts.isEcommerceActive) {
163
- let paymentsDone = false;
164
- try {
165
- const storeStatus = await getStoreConfigStatus();
166
- paymentsDone = storeStatus.stripe.hasKeys || storeStatus.freemius.hasKeys;
167
- } catch {
168
- paymentsDone = false;
169
- }
170
- // Insert Payments before the optional steps.
171
- steps.splice(4, 0, {
172
- key: 'payments',
173
- title: 'Set up payment providers',
174
- description: 'Add your Stripe and/or Freemius API keys to accept payments.',
175
- href: '/cms/payments',
176
- done: paymentsDone,
177
- optional: false,
178
- });
179
- }
180
-
181
- // Git-backed (Vercel 1-click / fork) installs: remind the operator to enable GitHub
182
- // Actions so the upstream sync workflow can run. "done" flips once the background poll
183
- // (maybeRefreshUpstreamStatus) has seen the workflow run at least once.
184
- if (detectChannel() === 'vercel') {
185
- let actionsActive = false;
186
- try {
187
- const config = await getSystemConfiguration();
188
- const upstream = config.settings?.['upstream_status'] as
189
- | { actions_active?: boolean }
190
- | undefined;
191
- actionsActive = upstream?.actions_active === true;
192
- } catch {
193
- actionsActive = false;
194
- }
195
- const canConnect = !actionsActive && isGithubConnectAvailable();
196
- steps.push({
197
- key: 'github-actions',
198
- title: 'Automatic updates (GitHub Actions)',
199
- description: actionsActive
200
- ? 'The daily upstream-sync workflow is active for your repository.'
201
- : canConnect
202
- ? 'Connect GitHub to install the upstream-sync workflow into your repo — Vercel’s 1-click deploy can’t copy it automatically.'
203
- : 'Vercel deploys have GitHub Actions on by default — this completes once GitHub registers the sync workflow on your default branch. A manually forked repo needs Actions enabled under Settings → Actions.',
204
- href:
205
- selfActionsSettingsUrl() ??
206
- 'https://github.com/nextblock-cms/nextblock/blob/HEAD/docs/13-STAYING-UP-TO-DATE.md',
207
- done: actionsActive,
208
- optional: true,
209
- isExternal: true,
210
- connectGithub: canConnect,
211
- });
212
- }
213
-
214
- const completed = steps.filter((s) => s.done).length;
215
-
216
- return {
217
- steps,
218
- completed,
219
- total: steps.length,
220
- dismissed: onboardingState['dismissed'] === true,
221
- };
222
- }
1
+ import 'server-only';
2
+ // Derives the dashboard onboarding checklist live from configuration state. Every check
3
+ // reads public/service-role data only, so it works for both ADMIN and WRITER dashboards.
4
+ import { createClient } from '@nextblock-cms/db/server';
5
+ import { getStoreConfigStatus } from '@nextblock-cms/ecommerce/server';
6
+ import { getEmailPublicSettings } from '../config/email-settings';
7
+ import { getPrivacySettings } from '../privacy/settings';
8
+ import { detectChannel } from '../setup/env-status';
9
+ import { getSystemConfiguration } from '../setup/system-config';
10
+ import { selfActionsSettingsUrl } from '../updates/repo-identity';
11
+ import { isGithubConnectAvailable } from '../updates/github-device';
12
+
13
+ export type OnboardingStep = {
14
+ key: string;
15
+ title: string;
16
+ description: string;
17
+ href: string;
18
+ done: boolean;
19
+ optional: boolean;
20
+ /** When true, render the CTA as an external link (new tab) instead of an in-app route. */
21
+ isExternal?: boolean;
22
+ /** When true, render the device-flow "Connect GitHub" control instead of a link. */
23
+ connectGithub?: boolean;
24
+ };
25
+
26
+ export type OnboardingStatus = {
27
+ steps: OnboardingStep[];
28
+ completed: number;
29
+ total: number;
30
+ dismissed: boolean;
31
+ };
32
+
33
+ // Seeded defaults (libs/db baseline seed 00000000000003 + migration 00000000000004). The
34
+ // branding/copyright steps count as "done" only when the value has been customized away from
35
+ // these — a fresh install ships with the seeds, so a plain presence check would mark every
36
+ // step complete immediately.
37
+ const SEEDED_SITE_TITLE = 'NextBlock™ CMS';
38
+ // Every object_key a fresh install may ship as the DEFAULT logo — the original baseline WebP
39
+ // and the migration-00000000000004 email-safe PNG (which repoints the seeded default). A logo
40
+ // counts as customized only when it is none of these; a new default swap must be added here or
41
+ // branding falsely reads as done out of the box. Mirrors BUNDLED_PUBLIC_MEDIA_KEYS in
42
+ // lib/media/resolveMediaUrl.ts.
43
+ const SEEDED_LOGO_OBJECT_KEYS = new Set<string>([
44
+ 'images/nextblock-logo-small.webp',
45
+ 'images/nextblock-logo-button-tiny.png',
46
+ ]);
47
+ const SEEDED_COPYRIGHT: Record<string, string> = {
48
+ en: '© {year} Nextblock CMS. All rights reserved.',
49
+ fr: '© {year} Nextblock CMS. Tous droits réservés.',
50
+ };
51
+
52
+ /** True when at least one language's copyright text is set AND differs from the seed. */
53
+ function hasCustomCopyright(value: unknown): boolean {
54
+ if (!value || typeof value !== 'object') return false;
55
+ return Object.entries(value as Record<string, unknown>).some(([lang, v]) => {
56
+ if (typeof v !== 'string') return false;
57
+ const trimmed = v.trim();
58
+ return trimmed.length > 0 && trimmed !== (SEEDED_COPYRIGHT[lang] ?? '');
59
+ });
60
+ }
61
+
62
+ /** Pull the active logo's media object_key from the (to-one) embedded relation. */
63
+ function extractLogoObjectKey(logoRow: unknown): string | null {
64
+ if (!logoRow || typeof logoRow !== 'object') return null;
65
+ const media = (logoRow as { media?: unknown }).media;
66
+ const record = Array.isArray(media) ? media[0] : media;
67
+ if (!record || typeof record !== 'object') return null;
68
+ const key = (record as { object_key?: unknown }).object_key;
69
+ return typeof key === 'string' ? key : null;
70
+ }
71
+
72
+ export async function getOnboardingStatus(opts: {
73
+ isEcommerceActive: boolean;
74
+ }): Promise<OnboardingStatus> {
75
+ const supabase = createClient();
76
+
77
+ const [{ data: settingRows }, { data: logoRow }, emailPublic, privacy] = await Promise.all([
78
+ supabase
79
+ .from('site_settings')
80
+ .select('key, value')
81
+ .in('key', ['footer_copyright', 'bot_protection_public', 'onboarding_state', 'site_title']),
82
+ supabase
83
+ .from('logos')
84
+ .select('media:media_id(object_key)')
85
+ .order('created_at', { ascending: false })
86
+ .limit(1)
87
+ .maybeSingle(),
88
+ getEmailPublicSettings(),
89
+ getPrivacySettings(),
90
+ ]);
91
+
92
+ const rows = new Map((settingRows ?? []).map((r) => [r.key, r.value]));
93
+ const botPublic = (rows.get('bot_protection_public') ?? {}) as Record<string, unknown>;
94
+ const onboardingState = (rows.get('onboarding_state') ?? {}) as Record<string, unknown>;
95
+
96
+ const siteTitleRaw = rows.get('site_title');
97
+ const siteTitle = typeof siteTitleRaw === 'string' ? siteTitleRaw.trim() : '';
98
+ const siteTitleCustomized = siteTitle.length > 0 && siteTitle !== SEEDED_SITE_TITLE;
99
+ const logoObjectKey = extractLogoObjectKey(logoRow);
100
+ const logoCustomized = Boolean(logoObjectKey) && !SEEDED_LOGO_OBJECT_KEYS.has(logoObjectKey ?? '');
101
+
102
+ // Branding is "done" once the user renames the site or uploads their own logo — not merely
103
+ // because the seeded NextBlock title/logo exist.
104
+ const brandingDone = siteTitleCustomized || logoCustomized;
105
+ const copyrightDone = hasCustomCopyright(rows.get('footer_copyright'));
106
+ const emailDone = Boolean(emailPublic.host) || Boolean(process.env['SMTP_HOST']);
107
+ const analyticsDone = Boolean(privacy.gtm_id);
108
+ const botProvider = typeof botPublic['provider'] === 'string' ? (botPublic['provider'] as string) : 'none';
109
+ const botDone = botProvider !== 'none' && botProvider !== '';
110
+
111
+ const steps: OnboardingStep[] = [
112
+ {
113
+ key: 'admin',
114
+ title: 'Create your admin account',
115
+ description: 'Your administrator account is set up.',
116
+ href: '/cms/users',
117
+ done: true,
118
+ optional: false,
119
+ },
120
+ {
121
+ key: 'branding',
122
+ title: 'Add your branding',
123
+ description: 'Upload your logo and set your site identity.',
124
+ href: '/cms/settings/logos',
125
+ done: brandingDone,
126
+ optional: false,
127
+ },
128
+ {
129
+ key: 'copyright',
130
+ title: 'Set your copyright / footer',
131
+ description: 'Configure the footer copyright shown across your site.',
132
+ href: '/cms/settings/copyright',
133
+ done: copyrightDone,
134
+ optional: false,
135
+ },
136
+ {
137
+ key: 'email',
138
+ title: 'Configure email (SMTP)',
139
+ description: 'Enable verification emails, password resets, and notifications.',
140
+ href: '/cms/settings/email',
141
+ done: emailDone,
142
+ optional: false,
143
+ },
144
+ {
145
+ key: 'analytics',
146
+ title: 'Connect analytics',
147
+ description: 'Add a Google Tag Manager ID for consent-gated analytics.',
148
+ href: '/cms/settings/google-analytics',
149
+ done: analyticsDone,
150
+ optional: true,
151
+ },
152
+ {
153
+ key: 'bot',
154
+ title: 'Enable bot protection',
155
+ description: 'Protect your sign-up and sign-in forms with a CAPTCHA.',
156
+ href: '/cms/settings/bot-protection',
157
+ done: botDone,
158
+ optional: true,
159
+ },
160
+ ];
161
+
162
+ if (opts.isEcommerceActive) {
163
+ let paymentsDone = false;
164
+ try {
165
+ const storeStatus = await getStoreConfigStatus();
166
+ paymentsDone = storeStatus.stripe.hasKeys || storeStatus.freemius.hasKeys;
167
+ } catch {
168
+ paymentsDone = false;
169
+ }
170
+ // Insert Payments before the optional steps.
171
+ steps.splice(4, 0, {
172
+ key: 'payments',
173
+ title: 'Set up payment providers',
174
+ description: 'Add your Stripe and/or Freemius API keys to accept payments.',
175
+ href: '/cms/payments',
176
+ done: paymentsDone,
177
+ optional: false,
178
+ });
179
+ }
180
+
181
+ // Git-backed (Vercel 1-click / fork) installs: remind the operator to enable GitHub
182
+ // Actions so the upstream sync workflow can run. "done" flips once the background poll
183
+ // (maybeRefreshUpstreamStatus) has seen the workflow run at least once.
184
+ if (detectChannel() === 'vercel') {
185
+ let actionsActive = false;
186
+ try {
187
+ const config = await getSystemConfiguration();
188
+ const upstream = config.settings?.['upstream_status'] as
189
+ | { actions_active?: boolean }
190
+ | undefined;
191
+ actionsActive = upstream?.actions_active === true;
192
+ } catch {
193
+ actionsActive = false;
194
+ }
195
+ const canConnect = !actionsActive && isGithubConnectAvailable();
196
+ steps.push({
197
+ key: 'github-actions',
198
+ title: 'Automatic updates (GitHub Actions)',
199
+ description: actionsActive
200
+ ? 'The daily upstream-sync workflow is active for your repository.'
201
+ : canConnect
202
+ ? 'Connect GitHub to install the upstream-sync workflow into your repo — Vercel’s 1-click deploy can’t copy it automatically.'
203
+ : 'Vercel deploys have GitHub Actions on by default — this completes once GitHub registers the sync workflow on your default branch. A manually forked repo needs Actions enabled under Settings → Actions.',
204
+ href:
205
+ selfActionsSettingsUrl() ??
206
+ 'https://github.com/nextblock-cms/nextblock/blob/HEAD/docs/13-STAYING-UP-TO-DATE.md',
207
+ done: actionsActive,
208
+ optional: true,
209
+ isExternal: true,
210
+ connectGithub: canConnect,
211
+ });
212
+ }
213
+
214
+ const completed = steps.filter((s) => s.done).length;
215
+
216
+ return {
217
+ steps,
218
+ completed,
219
+ total: steps.length,
220
+ dismissed: onboardingState['dismissed'] === true,
221
+ };
222
+ }
@@ -1,60 +1,60 @@
1
- import type { Database } from '@nextblock-cms/db';
2
-
3
- type BlockRow = Database['public']['Tables']['blocks']['Row'];
4
-
5
- const WORDS_PER_MINUTE = 200;
6
-
7
- function extractPlainTextFromHtml(html: string) {
8
- return html
9
- .replace(/<style[\s\S]*?<\/style>/gi, ' ')
10
- .replace(/<script[\s\S]*?<\/script>/gi, ' ')
11
- .replace(/<[^>]+>/g, ' ')
12
- .replace(/&nbsp;/g, ' ')
13
- .replace(/&amp;/g, '&')
14
- .replace(/\s+/g, ' ')
15
- .trim();
16
- }
17
-
18
- function countWords(text: string) {
19
- if (!text) {
20
- return 0;
21
- }
22
-
23
- return text.split(' ').length;
24
- }
25
-
26
- export function estimateReadTimeMinutesFromHtmlFragments(htmlFragments: string[]) {
27
- const words = htmlFragments.reduce((total, html) => {
28
- const plainText = extractPlainTextFromHtml(html);
29
- return total + countWords(plainText);
30
- }, 0);
31
-
32
- return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
33
- }
34
-
35
- export function estimateReadTimeMinutesFromBlocks(
36
- blocks: Array<Pick<BlockRow, 'block_type' | 'content'>> | undefined
37
- ) {
38
- if (!blocks || blocks.length === 0) {
39
- return 1;
40
- }
41
-
42
- const htmlFragments = blocks.reduce<string[]>((fragments, block) => {
43
- if (block.block_type !== 'text') {
44
- return fragments;
45
- }
46
-
47
- const html =
48
- typeof block.content === 'object' && block.content && 'html_content' in block.content
49
- ? String((block.content as { html_content?: string }).html_content ?? '')
50
- : '';
51
-
52
- if (html) {
53
- fragments.push(html);
54
- }
55
-
56
- return fragments;
57
- }, []);
58
-
59
- return estimateReadTimeMinutesFromHtmlFragments(htmlFragments);
60
- }
1
+ import type { Database } from '@nextblock-cms/db';
2
+
3
+ type BlockRow = Database['public']['Tables']['blocks']['Row'];
4
+
5
+ const WORDS_PER_MINUTE = 200;
6
+
7
+ function extractPlainTextFromHtml(html: string) {
8
+ return html
9
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
10
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
11
+ .replace(/<[^>]+>/g, ' ')
12
+ .replace(/&nbsp;/g, ' ')
13
+ .replace(/&amp;/g, '&')
14
+ .replace(/\s+/g, ' ')
15
+ .trim();
16
+ }
17
+
18
+ function countWords(text: string) {
19
+ if (!text) {
20
+ return 0;
21
+ }
22
+
23
+ return text.split(' ').length;
24
+ }
25
+
26
+ export function estimateReadTimeMinutesFromHtmlFragments(htmlFragments: string[]) {
27
+ const words = htmlFragments.reduce((total, html) => {
28
+ const plainText = extractPlainTextFromHtml(html);
29
+ return total + countWords(plainText);
30
+ }, 0);
31
+
32
+ return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
33
+ }
34
+
35
+ export function estimateReadTimeMinutesFromBlocks(
36
+ blocks: Array<Pick<BlockRow, 'block_type' | 'content'>> | undefined
37
+ ) {
38
+ if (!blocks || blocks.length === 0) {
39
+ return 1;
40
+ }
41
+
42
+ const htmlFragments = blocks.reduce<string[]>((fragments, block) => {
43
+ if (block.block_type !== 'text') {
44
+ return fragments;
45
+ }
46
+
47
+ const html =
48
+ typeof block.content === 'object' && block.content && 'html_content' in block.content
49
+ ? String((block.content as { html_content?: string }).html_content ?? '')
50
+ : '';
51
+
52
+ if (html) {
53
+ fragments.push(html);
54
+ }
55
+
56
+ return fragments;
57
+ }, []);
58
+
59
+ return estimateReadTimeMinutesFromHtmlFragments(htmlFragments);
60
+ }