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.
- package/docker-template/.dockerignore +1 -1
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -221
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -3092
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/api/view/route.ts +114 -114
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -123
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
|
@@ -1,259 +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
|
+
// 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
|
+
}
|
|
@@ -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
|
+
}
|