create-nextblock 0.14.1 → 0.14.4

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 (263) 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 +5881 -5623
  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/CmsClientLayout.tsx +2 -2
  44. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +17 -22
  45. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  46. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +13 -19
  47. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  48. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  49. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +45 -34
  50. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +4 -2
  51. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  52. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  53. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  54. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  55. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  56. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  57. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  58. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  59. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  60. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  61. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  62. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  63. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  64. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  65. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  66. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  67. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  68. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  69. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  70. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  71. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  72. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  73. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  75. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  76. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  77. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  78. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  79. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  80. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  81. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  82. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  83. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  86. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  87. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  88. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +688 -688
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  95. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -0
  96. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -0
  97. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -24
  98. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -0
  99. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  100. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  101. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  102. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  103. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +23 -21
  104. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  105. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  106. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  107. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  108. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  109. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  110. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +9 -7
  111. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  112. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  113. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/layout.tsx +49 -0
  116. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  117. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  118. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  119. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  120. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  121. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  122. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  123. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  124. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  125. package/templates/nextblock-template/app/providers.tsx +16 -4
  126. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  127. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  128. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  129. package/templates/nextblock-template/app/sitemap.ts +130 -130
  130. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  131. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  132. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  133. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  134. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  135. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  136. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  137. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  138. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  139. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  140. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  141. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  144. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  145. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  146. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +7 -10
  147. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  148. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  149. package/templates/nextblock-template/components/form-message.tsx +32 -32
  150. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  151. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  152. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  153. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  154. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  155. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  156. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  157. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  158. package/templates/nextblock-template/components/theme-icon.tsx +78 -0
  159. package/templates/nextblock-template/components/theme-switcher.tsx +77 -82
  160. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  161. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  162. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -0
  163. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  164. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  165. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  166. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  167. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  168. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  169. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +279 -202
  170. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  171. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  172. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  173. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  174. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  175. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  176. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  177. package/templates/nextblock-template/docs/README.md +39 -39
  178. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  179. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  180. package/templates/nextblock-template/index.d.ts +7 -7
  181. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  182. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  183. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  184. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  185. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  186. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  187. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +114 -0
  188. package/templates/nextblock-template/lib/blocks/blockColors.ts +132 -0
  189. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +17 -1
  190. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  191. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  192. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  193. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  194. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  195. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  196. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  197. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  198. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  199. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  200. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  201. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  202. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  203. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  204. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  205. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  206. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  207. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  208. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  209. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  210. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  211. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  212. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  213. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  214. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  215. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  216. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  217. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  218. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  219. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  220. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  221. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  222. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  223. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  224. package/templates/nextblock-template/lib/search/types.ts +27 -27
  225. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  226. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  227. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  228. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +15 -10
  229. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  230. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  231. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  232. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  233. package/templates/nextblock-template/lib/site-url.ts +48 -48
  234. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  235. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  236. package/templates/nextblock-template/lib/themes/buildThemeCss.test.ts +163 -0
  237. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -0
  238. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -0
  239. package/templates/nextblock-template/lib/themes/tokens.ts +143 -0
  240. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  241. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  242. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  243. package/templates/nextblock-template/next-env.d.ts +2 -2
  244. package/templates/nextblock-template/package.json +1 -1
  245. package/templates/nextblock-template/postcss.config.js +6 -6
  246. package/templates/nextblock-template/scripts/backup.js +115 -115
  247. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  248. package/templates/nextblock-template/scripts/restore.js +385 -385
  249. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  250. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  251. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  252. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  253. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  254. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  255. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  256. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  257. package/templates/nextblock-template/scripts/verify-site-themes.ts +63 -0
  258. package/templates/nextblock-template/tailwind.config.js +25 -25
  259. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  260. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  261. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  262. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  263. package/tsconfig.base.json +3 -3
@@ -0,0 +1,267 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import {
6
+ Button,
7
+ Input,
8
+ Label,
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from '@nextblock-cms/ui';
16
+ import { toast } from 'sonner';
17
+ import { Copy, Loader2, Plus, Star, Trash2 } from 'lucide-react';
18
+ import type { SiteTheme } from '../../../../../lib/themes/buildThemeCss';
19
+ import {
20
+ createTheme,
21
+ deleteTheme,
22
+ duplicateTheme,
23
+ setDefaultTheme,
24
+ updateTheme,
25
+ } from '../theme-actions';
26
+ import { ThemeEditor, ThemePreview, themeToDraft, type ThemeDraft } from './ThemeEditor';
27
+ import { ThemeIcon } from '../../../../../components/theme-icon';
28
+
29
+ export default function ThemeManager({ initialThemes }: { initialThemes: SiteTheme[] }) {
30
+ const router = useRouter();
31
+ const [themes, setThemes] = React.useState(initialThemes);
32
+ const [selectedId, setSelectedId] = React.useState(initialThemes[0]?.id ?? '');
33
+ const [draft, setDraft] = React.useState<ThemeDraft | null>(
34
+ initialThemes[0] ? themeToDraft(initialThemes[0]) : null,
35
+ );
36
+ const [pending, setPending] = React.useState<string | null>(null);
37
+ const [createOpen, setCreateOpen] = React.useState(false);
38
+ const [newName, setNewName] = React.useState('');
39
+
40
+ React.useEffect(() => {
41
+ setThemes(initialThemes);
42
+ }, [initialThemes]);
43
+
44
+ const selected = themes.find((theme) => theme.id === selectedId) ?? null;
45
+
46
+ // Switching themes discards nothing silently: the draft is per-theme and only
47
+ // written back on Save, so we reseed whenever the selection changes.
48
+ const selectTheme = (theme: SiteTheme) => {
49
+ setSelectedId(theme.id);
50
+ setDraft(themeToDraft(theme));
51
+ };
52
+
53
+ const isDirty = React.useMemo(() => {
54
+ if (!selected || !draft) return false;
55
+ return JSON.stringify(draft) !== JSON.stringify(themeToDraft(selected));
56
+ }, [selected, draft]);
57
+
58
+ const run = async (key: string, fn: () => Promise<{ ok: boolean; message?: string; error?: string }>) => {
59
+ setPending(key);
60
+ try {
61
+ const result = await fn();
62
+ if (result.ok) {
63
+ toast.success(result.message ?? 'Done.');
64
+ router.refresh();
65
+ return true;
66
+ }
67
+ toast.error(result.error ?? 'Something went wrong.');
68
+ return false;
69
+ } catch (error) {
70
+ toast.error(error instanceof Error ? error.message : 'Something went wrong.');
71
+ return false;
72
+ } finally {
73
+ setPending(null);
74
+ }
75
+ };
76
+
77
+ const handleSave = async () => {
78
+ if (!selected || !draft) return;
79
+ await run('save', () =>
80
+ updateTheme(selected.id, {
81
+ name: draft.name,
82
+ description: draft.description,
83
+ icon: draft.icon,
84
+ color_scheme: draft.color_scheme,
85
+ tokens: draft.tokens,
86
+ extra_css: draft.extra_css,
87
+ is_active: selected.is_default ? true : draft.is_active,
88
+ }),
89
+ );
90
+ };
91
+
92
+ const handleCreate = async () => {
93
+ const name = newName.trim();
94
+ if (!name) return;
95
+ const ok = await run('create', () =>
96
+ createTheme({
97
+ name,
98
+ // Start from the currently selected theme's palette so a new theme is a
99
+ // usable variation rather than an unreadable blank slate.
100
+ tokens: draft?.tokens ?? {},
101
+ color_scheme: draft?.color_scheme ?? 'light',
102
+ icon: 'Palette',
103
+ sort_order: (themes[themes.length - 1]?.sort_order ?? 0) + 10,
104
+ }),
105
+ );
106
+ if (ok) {
107
+ setCreateOpen(false);
108
+ setNewName('');
109
+ }
110
+ };
111
+
112
+ if (!selected || !draft) {
113
+ return (
114
+ <div className="rounded-lg border border-dashed p-8 text-center">
115
+ <p className="text-sm text-muted-foreground">
116
+ No themes found. Run the pending database migration to seed Light, Dark and Vibrant.
117
+ </p>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ return (
123
+ <div className="space-y-4">
124
+ <div className="grid gap-6 lg:grid-cols-[220px_minmax(0,1fr)]">
125
+ {/* Theme list */}
126
+ <aside className="space-y-2">
127
+ <div className="flex items-center justify-between">
128
+ <h2 className="text-sm font-semibold">Themes</h2>
129
+ <Button type="button" variant="ghost" size="sm" className="h-7 px-2" onClick={() => setCreateOpen(true)}>
130
+ <Plus className="h-3.5 w-3.5" />
131
+ </Button>
132
+ </div>
133
+ <ul className="space-y-1">
134
+ {themes.map((theme) => {
135
+ const active = theme.id === selectedId;
136
+ return (
137
+ <li key={theme.id}>
138
+ <button
139
+ type="button"
140
+ onClick={() => selectTheme(theme)}
141
+ aria-current={active}
142
+ className={
143
+ 'flex w-full items-center gap-2 rounded-md border px-2.5 py-2 text-left text-sm transition-colors ' +
144
+ (active ? 'border-ring bg-accent/50' : 'border-transparent hover:bg-accent/30')
145
+ }
146
+ >
147
+ <ThemeIcon name={theme.icon} size={15} className="shrink-0 text-muted-foreground" />
148
+ <span className="min-w-0 flex-1 truncate">{theme.name}</span>
149
+ {theme.is_default ? (
150
+ <Star className="h-3.5 w-3.5 shrink-0 fill-current text-amber-500" aria-label="Default theme" />
151
+ ) : null}
152
+ {!theme.is_active ? (
153
+ <span className="shrink-0 text-[10px] text-muted-foreground">hidden</span>
154
+ ) : null}
155
+ </button>
156
+ </li>
157
+ );
158
+ })}
159
+ </ul>
160
+ </aside>
161
+
162
+ {/* Editor */}
163
+ <div className="min-w-0 space-y-4">
164
+ <div className="flex flex-wrap items-center gap-2 border-b pb-3">
165
+ <Button type="button" onClick={handleSave} disabled={!isDirty || pending !== null}>
166
+ {pending === 'save' ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : null}
167
+ {isDirty ? 'Save changes' : 'Saved'}
168
+ </Button>
169
+ <Button
170
+ type="button"
171
+ variant="outline"
172
+ size="sm"
173
+ disabled={pending !== null}
174
+ onClick={() => run('duplicate', () => duplicateTheme(selected.id))}
175
+ >
176
+ <Copy className="mr-1.5 h-3.5 w-3.5" />
177
+ Duplicate
178
+ </Button>
179
+ {!selected.is_default ? (
180
+ <Button
181
+ type="button"
182
+ variant="outline"
183
+ size="sm"
184
+ disabled={pending !== null}
185
+ onClick={() => run('default', () => setDefaultTheme(selected.id))}
186
+ >
187
+ <Star className="mr-1.5 h-3.5 w-3.5" />
188
+ Make default
189
+ </Button>
190
+ ) : null}
191
+ {!selected.is_system && !selected.is_default ? (
192
+ <Button
193
+ type="button"
194
+ variant="outline"
195
+ size="sm"
196
+ disabled={pending !== null}
197
+ className="text-destructive hover:text-destructive"
198
+ onClick={() => {
199
+ if (
200
+ !window.confirm(
201
+ `Delete "${selected.name}"? Visitors currently using it will fall back to the default theme.`,
202
+ )
203
+ ) {
204
+ return;
205
+ }
206
+ void run('delete', () => deleteTheme(selected.id)).then((ok) => {
207
+ if (ok) {
208
+ const next = themes.find((theme) => theme.id !== selected.id);
209
+ if (next) selectTheme(next);
210
+ }
211
+ });
212
+ }}
213
+ >
214
+ <Trash2 className="mr-1.5 h-3.5 w-3.5" />
215
+ Delete
216
+ </Button>
217
+ ) : null}
218
+ {selected.is_system ? (
219
+ <span className="text-[11px] text-muted-foreground">
220
+ System theme — fully editable, but cannot be deleted because &quot;System&quot; resolves to it.
221
+ </span>
222
+ ) : null}
223
+ </div>
224
+
225
+ <ThemePreview draft={draft} />
226
+
227
+ <ThemeEditor theme={selected} draft={draft} onDraftChange={setDraft} />
228
+ </div>
229
+ </div>
230
+
231
+ <Dialog open={createOpen} onOpenChange={setCreateOpen}>
232
+ <DialogContent>
233
+ <DialogHeader>
234
+ <DialogTitle>New theme</DialogTitle>
235
+ <DialogDescription>
236
+ Starts as a copy of &quot;{selected.name}&quot; so you can adjust from a working palette.
237
+ </DialogDescription>
238
+ </DialogHeader>
239
+ <div className="space-y-1.5">
240
+ <Label htmlFor="new-theme-name">Name</Label>
241
+ <Input
242
+ id="new-theme-name"
243
+ value={newName}
244
+ onChange={(event) => setNewName(event.target.value)}
245
+ placeholder="Midnight"
246
+ onKeyDown={(event) => {
247
+ if (event.key === 'Enter') {
248
+ event.preventDefault();
249
+ void handleCreate();
250
+ }
251
+ }}
252
+ />
253
+ </div>
254
+ <DialogFooter>
255
+ <Button type="button" variant="outline" onClick={() => setCreateOpen(false)}>
256
+ Cancel
257
+ </Button>
258
+ <Button type="button" onClick={handleCreate} disabled={!newName.trim() || pending !== null}>
259
+ {pending === 'create' ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : null}
260
+ Create theme
261
+ </Button>
262
+ </DialogFooter>
263
+ </DialogContent>
264
+ </Dialog>
265
+ </div>
266
+ );
267
+ }
@@ -1,24 +1,40 @@
1
- // app/cms/settings/global-css/page.tsx
2
- import { getGlobalCss } from './actions';
3
- import GlobalCssForm from './components/GlobalCssForm';
4
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextblock-cms/ui';
5
-
6
- export default async function GlobalCssSettingsPage() {
7
- const css = await getGlobalCss();
8
-
9
- return (
10
- <div className="max-w-4xl mx-auto">
11
- <Card>
12
- <CardHeader>
13
- <CardTitle>Global CSS</CardTitle>
14
- <CardDescription>
15
- Inject custom CSS rules dynamically across the entire application front-end.
16
- </CardDescription>
17
- </CardHeader>
18
- <CardContent>
19
- <GlobalCssForm initialCss={css} />
20
- </CardContent>
21
- </Card>
22
- </div>
23
- );
24
- }
1
+ // app/cms/settings/global-css/page.tsx
2
+ import { getGlobalCss } from './actions';
3
+ import { getSiteThemes } from './theme-actions';
4
+ import GlobalCssForm from './components/GlobalCssForm';
5
+ import ThemeManager from './components/ThemeManager';
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextblock-cms/ui';
7
+
8
+ export default async function GlobalCssSettingsPage() {
9
+ const [css, themes] = await Promise.all([getGlobalCss(), getSiteThemes()]);
10
+
11
+ return (
12
+ <div className="mx-auto max-w-6xl space-y-6">
13
+ <Card>
14
+ <CardHeader>
15
+ <CardTitle>Themes</CardTitle>
16
+ <CardDescription>
17
+ Recolour the themes visitors can pick from the switcher, or add your own. Changes apply
18
+ site-wide without a redeploy.
19
+ </CardDescription>
20
+ </CardHeader>
21
+ <CardContent>
22
+ <ThemeManager initialThemes={themes} />
23
+ </CardContent>
24
+ </Card>
25
+
26
+ <Card>
27
+ <CardHeader>
28
+ <CardTitle>Global CSS</CardTitle>
29
+ <CardDescription>
30
+ Inject custom CSS rules dynamically across the entire application front-end. Loaded after
31
+ the themes above, so it can override any of them.
32
+ </CardDescription>
33
+ </CardHeader>
34
+ <CardContent>
35
+ <GlobalCssForm initialCss={css} />
36
+ </CardContent>
37
+ </Card>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,259 @@
1
+ // app/cms/settings/global-css/theme-actions.ts
2
+ 'use server';
3
+
4
+ import { createClient } from '@nextblock-cms/db/server';
5
+ import { revalidatePath, revalidateTag } from 'next/cache';
6
+ import type { SettingsActionResult } from '../../../../lib/cms/action-result';
7
+ import { isValidThemeSlug, type SiteTheme } from '../../../../lib/themes/buildThemeCss';
8
+ import { isThemeTokenKey, isValidTokenValue, THEME_TOKEN_KEYS } from '../../../../lib/themes/tokens';
9
+
10
+ const THEME_COLUMNS =
11
+ 'id, slug, name, description, icon, color_scheme, tokens, extra_css, is_system, is_default, is_active, sort_order';
12
+
13
+ /** Theme edits are ADMIN-only — RLS enforces it too, this is the friendly error. */
14
+ async function requireAdmin() {
15
+ const supabase = createClient();
16
+ const {
17
+ data: { user },
18
+ } = await supabase.auth.getUser();
19
+ if (!user) {
20
+ return { supabase, error: 'You must be logged in to manage themes.' as const };
21
+ }
22
+
23
+ const { data: profile, error: profileError } = await supabase
24
+ .from('profiles')
25
+ .select('role')
26
+ .eq('id', user.id)
27
+ .single();
28
+
29
+ if (profileError || !profile || profile.role !== 'ADMIN') {
30
+ return { supabase, error: 'Only administrators can manage themes.' as const };
31
+ }
32
+ return { supabase, error: null };
33
+ }
34
+
35
+ function revalidateThemes() {
36
+ revalidateTag('public-layout-site-themes', 'max');
37
+ revalidatePath('/', 'layout');
38
+ }
39
+
40
+ export async function getSiteThemes(): Promise<SiteTheme[]> {
41
+ const supabase = createClient();
42
+ const { data, error } = await supabase.from('site_themes').select(THEME_COLUMNS).order('sort_order');
43
+ if (error || !data) return [];
44
+ return data as unknown as SiteTheme[];
45
+ }
46
+
47
+ /**
48
+ * Keep only known tokens with well-formed values. Anything else is dropped
49
+ * rather than rejected, so a partially-filled form still saves what it can.
50
+ */
51
+ function sanitizeTokens(input: unknown): Record<string, string> {
52
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return {};
53
+ const out: Record<string, string> = {};
54
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
55
+ if (typeof value !== 'string') continue;
56
+ const trimmed = value.trim();
57
+ if (isThemeTokenKey(key) && isValidTokenValue(key, trimmed)) {
58
+ out[key] = trimmed;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
64
+ export interface ThemeInput {
65
+ name: string;
66
+ slug?: string;
67
+ description?: string | null;
68
+ icon?: string;
69
+ color_scheme?: 'light' | 'dark';
70
+ tokens?: Record<string, string>;
71
+ extra_css?: string | null;
72
+ is_active?: boolean;
73
+ is_default?: boolean;
74
+ sort_order?: number;
75
+ }
76
+
77
+ export async function createTheme(input: ThemeInput): Promise<SettingsActionResult & { slug?: string }> {
78
+ const { supabase, error: authError } = await requireAdmin();
79
+ if (authError) return { ok: false, error: authError };
80
+
81
+ const slug = (input.slug ?? input.name ?? '')
82
+ .toLowerCase()
83
+ .trim()
84
+ .replace(/[^a-z0-9]+/g, '-')
85
+ .replace(/^-+|-+$/g, '');
86
+
87
+ if (!isValidThemeSlug(slug)) {
88
+ return {
89
+ ok: false,
90
+ error: 'Theme id must be 2-40 characters, lowercase letters, numbers and dashes, starting with a letter.',
91
+ };
92
+ }
93
+ if (!input.name?.trim()) {
94
+ return { ok: false, error: 'Theme name is required.' };
95
+ }
96
+
97
+ const { error } = await supabase.from('site_themes').insert({
98
+ slug,
99
+ name: input.name.trim(),
100
+ description: input.description?.trim() || null,
101
+ icon: input.icon || 'Palette',
102
+ color_scheme: input.color_scheme === 'dark' ? 'dark' : 'light',
103
+ tokens: sanitizeTokens(input.tokens),
104
+ extra_css: input.extra_css?.trim() || null,
105
+ is_active: input.is_active ?? true,
106
+ is_default: false,
107
+ is_system: false,
108
+ sort_order: input.sort_order ?? 100,
109
+ });
110
+
111
+ if (error) {
112
+ if (error.code === '23505') {
113
+ return { ok: false, error: `A theme with the id "${slug}" already exists.` };
114
+ }
115
+ console.error('Error creating theme:', error);
116
+ return { ok: false, error: 'Failed to create theme.' };
117
+ }
118
+
119
+ revalidateThemes();
120
+ return { ok: true, message: `Theme "${input.name.trim()}" created.`, slug };
121
+ }
122
+
123
+ export async function updateTheme(id: string, input: ThemeInput): Promise<SettingsActionResult> {
124
+ const { supabase, error: authError } = await requireAdmin();
125
+ if (authError) return { ok: false, error: authError };
126
+
127
+ if (!input.name?.trim()) {
128
+ return { ok: false, error: 'Theme name is required.' };
129
+ }
130
+
131
+ // `slug` and `is_system` are intentionally not updatable: the slug is the CSS
132
+ // class and the value persisted in each visitor's localStorage by next-themes,
133
+ // so renaming it would silently reset everyone's chosen theme.
134
+ const patch: Record<string, unknown> = {
135
+ name: input.name.trim(),
136
+ description: input.description?.trim() || null,
137
+ icon: input.icon || 'Palette',
138
+ color_scheme: input.color_scheme === 'dark' ? 'dark' : 'light',
139
+ extra_css: input.extra_css?.trim() || null,
140
+ };
141
+ if (input.tokens !== undefined) patch.tokens = sanitizeTokens(input.tokens);
142
+ if (input.is_active !== undefined) patch.is_active = input.is_active;
143
+ if (input.sort_order !== undefined) patch.sort_order = input.sort_order;
144
+
145
+ const { error } = await supabase.from('site_themes').update(patch).eq('id', id);
146
+
147
+ if (error) {
148
+ console.error('Error updating theme:', error);
149
+ return { ok: false, error: 'Failed to update theme.' };
150
+ }
151
+
152
+ revalidateThemes();
153
+ return { ok: true, message: 'Theme saved.' };
154
+ }
155
+
156
+ export async function setDefaultTheme(id: string): Promise<SettingsActionResult> {
157
+ const { supabase, error: authError } = await requireAdmin();
158
+ if (authError) return { ok: false, error: authError };
159
+
160
+ // A hidden theme cannot be the site default.
161
+ const { error } = await supabase
162
+ .from('site_themes')
163
+ .update({ is_default: true, is_active: true })
164
+ .eq('id', id);
165
+
166
+ if (error) {
167
+ console.error('Error setting default theme:', error);
168
+ return { ok: false, error: 'Failed to set the default theme.' };
169
+ }
170
+
171
+ revalidateThemes();
172
+ return { ok: true, message: 'Default theme updated.' };
173
+ }
174
+
175
+ export async function deleteTheme(id: string): Promise<SettingsActionResult> {
176
+ const { supabase, error: authError } = await requireAdmin();
177
+ if (authError) return { ok: false, error: authError };
178
+
179
+ const { data: theme, error: readError } = await supabase
180
+ .from('site_themes')
181
+ .select('slug, name, is_system, is_default')
182
+ .eq('id', id)
183
+ .single();
184
+
185
+ if (readError || !theme) {
186
+ return { ok: false, error: 'Theme not found.' };
187
+ }
188
+ if (theme.is_system) {
189
+ return {
190
+ ok: false,
191
+ error: `"${theme.name}" is a system theme. Light and Dark are what "System" resolves to, so they cannot be deleted — but you can recolour them freely.`,
192
+ };
193
+ }
194
+ if (theme.is_default) {
195
+ return { ok: false, error: 'Make another theme the default before deleting this one.' };
196
+ }
197
+
198
+ const { error } = await supabase.from('site_themes').delete().eq('id', id);
199
+ if (error) {
200
+ console.error('Error deleting theme:', error);
201
+ return { ok: false, error: 'Failed to delete theme.' };
202
+ }
203
+
204
+ revalidateThemes();
205
+ return { ok: true, message: `Theme "${theme.name}" deleted. Visitors using it fall back to the default.` };
206
+ }
207
+
208
+ export async function duplicateTheme(id: string): Promise<SettingsActionResult & { slug?: string }> {
209
+ const { supabase, error: authError } = await requireAdmin();
210
+ if (authError) return { ok: false, error: authError };
211
+
212
+ const { data: source, error: readError } = await supabase
213
+ .from('site_themes')
214
+ .select(THEME_COLUMNS)
215
+ .eq('id', id)
216
+ .single();
217
+
218
+ if (readError || !source) {
219
+ return { ok: false, error: 'Theme not found.' };
220
+ }
221
+
222
+ const theme = source as unknown as SiteTheme;
223
+ // Find a free slug: my-theme-copy, my-theme-copy-2, ...
224
+ const { data: existing } = await supabase.from('site_themes').select('slug');
225
+ const taken = new Set((existing ?? []).map((row) => row.slug));
226
+ let slug = `${theme.slug}-copy`.slice(0, 40);
227
+ let n = 2;
228
+ while (taken.has(slug)) {
229
+ slug = `${theme.slug}-copy-${n}`.slice(0, 40);
230
+ n += 1;
231
+ }
232
+
233
+ const { error } = await supabase.from('site_themes').insert({
234
+ slug,
235
+ name: `${theme.name} copy`,
236
+ description: theme.description,
237
+ icon: theme.icon,
238
+ color_scheme: theme.color_scheme,
239
+ tokens: sanitizeTokens(theme.tokens),
240
+ extra_css: theme.extra_css,
241
+ is_active: true,
242
+ is_default: false,
243
+ is_system: false,
244
+ sort_order: (theme.sort_order ?? 0) + 1,
245
+ });
246
+
247
+ if (error) {
248
+ console.error('Error duplicating theme:', error);
249
+ return { ok: false, error: 'Failed to duplicate theme.' };
250
+ }
251
+
252
+ revalidateThemes();
253
+ return { ok: true, message: `Created "${theme.name} copy".`, slug };
254
+ }
255
+
256
+ /** Exposed so the client form can render inputs for exactly what the server accepts. */
257
+ export async function getThemeTokenKeys(): Promise<string[]> {
258
+ return THEME_TOKEN_KEYS;
259
+ }
@@ -1,26 +1,26 @@
1
- // app/cms/settings/google-analytics/page.tsx
2
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextblock-cms/ui';
3
- import { getGoogleAnalyticsSettings } from './actions';
4
- import GoogleAnalyticsForm from './components/GoogleAnalyticsForm';
5
-
6
- export default async function GoogleAnalyticsSettingsPage() {
7
- const settings = await getGoogleAnalyticsSettings();
8
-
9
- return (
10
- <div className="max-w-4xl mx-auto">
11
- <Card>
12
- <CardHeader>
13
- <CardTitle>Google Analytics</CardTitle>
14
- <CardDescription>
15
- Configure Google Tag Manager and Google Analytics 4. These tags load only
16
- after a visitor accepts analytics in the Law 25 consent banner (managed under
17
- Privacy &amp; Consent), so the default page weight stays at zero tracking bytes.
18
- </CardDescription>
19
- </CardHeader>
20
- <CardContent>
21
- <GoogleAnalyticsForm initialSettings={settings} />
22
- </CardContent>
23
- </Card>
24
- </div>
25
- );
26
- }
1
+ // app/cms/settings/google-analytics/page.tsx
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextblock-cms/ui';
3
+ import { getGoogleAnalyticsSettings } from './actions';
4
+ import GoogleAnalyticsForm from './components/GoogleAnalyticsForm';
5
+
6
+ export default async function GoogleAnalyticsSettingsPage() {
7
+ const settings = await getGoogleAnalyticsSettings();
8
+
9
+ return (
10
+ <div className="max-w-4xl mx-auto">
11
+ <Card>
12
+ <CardHeader>
13
+ <CardTitle>Google Analytics</CardTitle>
14
+ <CardDescription>
15
+ Configure Google Tag Manager and Google Analytics 4. These tags load only
16
+ after a visitor accepts analytics in the Law 25 consent banner (managed under
17
+ Privacy &amp; Consent), so the default page weight stays at zero tracking bytes.
18
+ </CardDescription>
19
+ </CardHeader>
20
+ <CardContent>
21
+ <GoogleAnalyticsForm initialSettings={settings} />
22
+ </CardContent>
23
+ </Card>
24
+ </div>
25
+ );
26
+ }
@@ -10,10 +10,10 @@ export default async function EditLogoPage(props: { params: Promise<{ id: string
10
10
  notFound()
11
11
  }
12
12
 
13
- return (
14
- <div>
15
- <h1 className="text-2xl font-semibold mb-6">Edit Brand Logo</h1>
16
- <LogoForm logo={logo} action={updateLogo as any} />
17
- </div>
18
- )
19
- }
13
+ return (
14
+ <div>
15
+ <h1 className="text-2xl font-semibold mb-6">Edit Brand Logo</h1>
16
+ <LogoForm logo={logo} action={updateLogo as any} />
17
+ </div>
18
+ )
19
+ }