create-nextblock 0.14.1 → 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.
- package/docker-template/.dockerignore +23 -23
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- 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 +1 -1
- package/project.json +19 -19
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.dockerignore +23 -23
- package/templates/nextblock-template/README.md +34 -34
- 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/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/postActions.ts +146 -146
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5749
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- 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/article/[slug]/PostClientContent.tsx +441 -441
- 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/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +4 -2
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/layout.tsx +73 -73
- 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/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- 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/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
- package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +688 -688
- 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/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- 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/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +23 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +9 -7
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- 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/lib/site-settings.ts +105 -105
- 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/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/sitemap.ts +130 -130
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- 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/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- 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/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
- package/templates/nextblock-template/docs/README.md +39 -39
- 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/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -87
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- 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/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
-
import type { Database } from '@nextblock-cms/db';
|
|
3
|
-
|
|
4
|
-
// The single, shared definition of "the active logo", used by the public header, the CMS
|
|
5
|
-
// branding page, transactional-email branding, and invoices. The operator can pin a
|
|
6
|
-
// specific logo (stored as `site_settings.active_logo_id`); when none is pinned we fall
|
|
7
|
-
// back to the most recently created logo (the historical default). The setting key is a
|
|
8
|
-
// plain, non-secret `site_settings` row, so it is publicly readable under RLS (the public
|
|
9
|
-
// header can read it with the anon client) and writable by ADMIN/WRITER.
|
|
10
|
-
|
|
11
|
-
/** site_settings key holding the admin-selected active logo id (a `logos.id` uuid string). */
|
|
12
|
-
export const ACTIVE_LOGO_SETTING_KEY = 'active_logo_id';
|
|
13
|
-
|
|
14
|
-
// Accept any of the app's Supabase clients (request-scoped, static/anon, or service-role).
|
|
15
|
-
type AnyClient = SupabaseClient<Database>;
|
|
16
|
-
|
|
17
|
-
/** The admin-pinned active logo id, or null when none has been chosen. */
|
|
18
|
-
export async function resolveActiveLogoId(client: AnyClient): Promise<string | null> {
|
|
19
|
-
const { data } = await client
|
|
20
|
-
.from('site_settings')
|
|
21
|
-
.select('value')
|
|
22
|
-
.eq('key', ACTIVE_LOGO_SETTING_KEY)
|
|
23
|
-
.maybeSingle();
|
|
24
|
-
const value = data?.value;
|
|
25
|
-
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Resolve the active logo row (joined with its media) for the given client: the pinned
|
|
30
|
-
* logo when set and still present, otherwise the most recently created logo. Returns the
|
|
31
|
-
* raw row; callers cast to their local logo type. Returns null when there are no logos.
|
|
32
|
-
*/
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
-
export async function resolveActiveLogo(client: AnyClient): Promise<any> {
|
|
35
|
-
const activeId = await resolveActiveLogoId(client);
|
|
36
|
-
|
|
37
|
-
if (activeId) {
|
|
38
|
-
const { data } = await client
|
|
39
|
-
.from('logos')
|
|
40
|
-
.select('*, media:media_id (*)')
|
|
41
|
-
.eq('id', activeId)
|
|
42
|
-
.maybeSingle();
|
|
43
|
-
if (data) return data;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const { data } = await client
|
|
47
|
-
.from('logos')
|
|
48
|
-
.select('*, media:media_id (*)')
|
|
49
|
-
.order('created_at', { ascending: false })
|
|
50
|
-
.limit(1)
|
|
51
|
-
.maybeSingle();
|
|
52
|
-
return data ?? null;
|
|
53
|
-
}
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { Database } from '@nextblock-cms/db';
|
|
3
|
+
|
|
4
|
+
// The single, shared definition of "the active logo", used by the public header, the CMS
|
|
5
|
+
// branding page, transactional-email branding, and invoices. The operator can pin a
|
|
6
|
+
// specific logo (stored as `site_settings.active_logo_id`); when none is pinned we fall
|
|
7
|
+
// back to the most recently created logo (the historical default). The setting key is a
|
|
8
|
+
// plain, non-secret `site_settings` row, so it is publicly readable under RLS (the public
|
|
9
|
+
// header can read it with the anon client) and writable by ADMIN/WRITER.
|
|
10
|
+
|
|
11
|
+
/** site_settings key holding the admin-selected active logo id (a `logos.id` uuid string). */
|
|
12
|
+
export const ACTIVE_LOGO_SETTING_KEY = 'active_logo_id';
|
|
13
|
+
|
|
14
|
+
// Accept any of the app's Supabase clients (request-scoped, static/anon, or service-role).
|
|
15
|
+
type AnyClient = SupabaseClient<Database>;
|
|
16
|
+
|
|
17
|
+
/** The admin-pinned active logo id, or null when none has been chosen. */
|
|
18
|
+
export async function resolveActiveLogoId(client: AnyClient): Promise<string | null> {
|
|
19
|
+
const { data } = await client
|
|
20
|
+
.from('site_settings')
|
|
21
|
+
.select('value')
|
|
22
|
+
.eq('key', ACTIVE_LOGO_SETTING_KEY)
|
|
23
|
+
.maybeSingle();
|
|
24
|
+
const value = data?.value;
|
|
25
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the active logo row (joined with its media) for the given client: the pinned
|
|
30
|
+
* logo when set and still present, otherwise the most recently created logo. Returns the
|
|
31
|
+
* raw row; callers cast to their local logo type. Returns null when there are no logos.
|
|
32
|
+
*/
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
export async function resolveActiveLogo(client: AnyClient): Promise<any> {
|
|
35
|
+
const activeId = await resolveActiveLogoId(client);
|
|
36
|
+
|
|
37
|
+
if (activeId) {
|
|
38
|
+
const { data } = await client
|
|
39
|
+
.from('logos')
|
|
40
|
+
.select('*, media:media_id (*)')
|
|
41
|
+
.eq('id', activeId)
|
|
42
|
+
.maybeSingle();
|
|
43
|
+
if (data) return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { data } = await client
|
|
47
|
+
.from('logos')
|
|
48
|
+
.select('*, media:media_id (*)')
|
|
49
|
+
.order('created_at', { ascending: false })
|
|
50
|
+
.limit(1)
|
|
51
|
+
.maybeSingle();
|
|
52
|
+
return data ?? null;
|
|
53
|
+
}
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
const BUNDLED_PUBLIC_MEDIA_KEYS = new Set([
|
|
2
|
-
'images/NBcover.webp',
|
|
3
|
-
'images/cap.webp',
|
|
4
|
-
'images/commerce-plan.webp',
|
|
5
|
-
'images/commerce-square.webp',
|
|
6
|
-
'images/commerce-wide.webp',
|
|
7
|
-
'images/cortex-ai-square.webp',
|
|
8
|
-
'images/cortex-ai.webp',
|
|
9
|
-
'images/developer.webp',
|
|
10
|
-
'images/extensibility.webp',
|
|
11
|
-
'images/goals.webp',
|
|
12
|
-
'images/included.webp',
|
|
13
|
-
'images/metadata_image.webp',
|
|
14
|
-
'images/nextblock-logo-small.webp',
|
|
15
|
-
'images/nextblock-logo-button-tiny.png',
|
|
16
|
-
'images/nx-graph.webp',
|
|
17
|
-
'images/pants.webp',
|
|
18
|
-
'images/programmer-upscaled.webp',
|
|
19
|
-
'images/t-shirt.webp',
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
export function resolveMediaUrl(
|
|
23
|
-
objectKey?: string | null,
|
|
24
|
-
// On the client, NEXT_PUBLIC_R2_BASE_URL is inlined at build time, so a fresh-clone
|
|
25
|
-
// dev bundle built with no env holds an empty string; fall back to the runtime value
|
|
26
|
-
// injected by <PublicEnvBootstrap> (window.__NEXTBLOCK_PUBLIC_ENV__.r2Base). On the
|
|
27
|
-
// server, process.env is always current and the window branch is skipped.
|
|
28
|
-
baseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL ||
|
|
29
|
-
(typeof window !== 'undefined'
|
|
30
|
-
? (window as { __NEXTBLOCK_PUBLIC_ENV__?: { r2Base?: string } })
|
|
31
|
-
.__NEXTBLOCK_PUBLIC_ENV__?.r2Base || ''
|
|
32
|
-
: '')
|
|
33
|
-
) {
|
|
34
|
-
if (!objectKey) return null;
|
|
35
|
-
|
|
36
|
-
if (objectKey.startsWith('http://') || objectKey.startsWith('https://')) {
|
|
37
|
-
return objectKey;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (objectKey.startsWith('/')) {
|
|
41
|
-
return objectKey;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (BUNDLED_PUBLIC_MEDIA_KEYS.has(objectKey)) {
|
|
45
|
-
return `/${objectKey}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
49
|
-
const normalizedObjectKey = objectKey.replace(/^\/+/, '');
|
|
50
|
-
|
|
51
|
-
return normalizedBaseUrl
|
|
52
|
-
? `${normalizedBaseUrl}/${normalizedObjectKey}`
|
|
53
|
-
: `/${normalizedObjectKey}`;
|
|
54
|
-
}
|
|
1
|
+
const BUNDLED_PUBLIC_MEDIA_KEYS = new Set([
|
|
2
|
+
'images/NBcover.webp',
|
|
3
|
+
'images/cap.webp',
|
|
4
|
+
'images/commerce-plan.webp',
|
|
5
|
+
'images/commerce-square.webp',
|
|
6
|
+
'images/commerce-wide.webp',
|
|
7
|
+
'images/cortex-ai-square.webp',
|
|
8
|
+
'images/cortex-ai.webp',
|
|
9
|
+
'images/developer.webp',
|
|
10
|
+
'images/extensibility.webp',
|
|
11
|
+
'images/goals.webp',
|
|
12
|
+
'images/included.webp',
|
|
13
|
+
'images/metadata_image.webp',
|
|
14
|
+
'images/nextblock-logo-small.webp',
|
|
15
|
+
'images/nextblock-logo-button-tiny.png',
|
|
16
|
+
'images/nx-graph.webp',
|
|
17
|
+
'images/pants.webp',
|
|
18
|
+
'images/programmer-upscaled.webp',
|
|
19
|
+
'images/t-shirt.webp',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function resolveMediaUrl(
|
|
23
|
+
objectKey?: string | null,
|
|
24
|
+
// On the client, NEXT_PUBLIC_R2_BASE_URL is inlined at build time, so a fresh-clone
|
|
25
|
+
// dev bundle built with no env holds an empty string; fall back to the runtime value
|
|
26
|
+
// injected by <PublicEnvBootstrap> (window.__NEXTBLOCK_PUBLIC_ENV__.r2Base). On the
|
|
27
|
+
// server, process.env is always current and the window branch is skipped.
|
|
28
|
+
baseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL ||
|
|
29
|
+
(typeof window !== 'undefined'
|
|
30
|
+
? (window as { __NEXTBLOCK_PUBLIC_ENV__?: { r2Base?: string } })
|
|
31
|
+
.__NEXTBLOCK_PUBLIC_ENV__?.r2Base || ''
|
|
32
|
+
: '')
|
|
33
|
+
) {
|
|
34
|
+
if (!objectKey) return null;
|
|
35
|
+
|
|
36
|
+
if (objectKey.startsWith('http://') || objectKey.startsWith('https://')) {
|
|
37
|
+
return objectKey;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (objectKey.startsWith('/')) {
|
|
41
|
+
return objectKey;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (BUNDLED_PUBLIC_MEDIA_KEYS.has(objectKey)) {
|
|
45
|
+
return `/${objectKey}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
49
|
+
const normalizedObjectKey = objectKey.replace(/^\/+/, '');
|
|
50
|
+
|
|
51
|
+
return normalizedBaseUrl
|
|
52
|
+
? `${normalizedBaseUrl}/${normalizedObjectKey}`
|
|
53
|
+
: `/${normalizedObjectKey}`;
|
|
54
|
+
}
|
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
// Shared YouTube URL normalization.
|
|
2
|
-
//
|
|
3
|
-
// No public render path may emit a www.youtube.com iframe: the player writes
|
|
4
|
-
// cookies via document.cookie on load (TESTCOOKIESENABLED, LAST_RESULT_ENTRY_KEY,
|
|
5
|
-
// ExcludeSameSiteUnspecifiedTreatedAsLax) which Chrome logs as a DevTools "Cookie"
|
|
6
|
-
// issue and Lighthouse fails through the `inspector-issues` audit (Best Practices
|
|
7
|
-
// 26/27 = 96). Switching to youtube-nocookie.com does NOT stop those writes --
|
|
8
|
-
// only not loading the player does. See components/media/YouTubeFacade.tsx.
|
|
9
|
-
|
|
10
|
-
const YOUTUBE_HOST_RE = /^(?:www\.|m\.|music\.)?(?:youtube\.com|youtube-nocookie\.com)$/i;
|
|
11
|
-
const YOUTU_BE_RE = /^(?:www\.)?youtu\.be$/i;
|
|
12
|
-
const VIDEO_ID_RE = /^[A-Za-z0-9_-]{6,20}$/;
|
|
13
|
-
|
|
14
|
-
export interface ParsedYouTubeUrl {
|
|
15
|
-
videoId: string;
|
|
16
|
-
/** Extra player params carried over from the source URL (v/si/autoplay stripped). */
|
|
17
|
-
params: URLSearchParams;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Accepts every shape an author can paste or that YouTube's Share > Embed panel
|
|
22
|
-
* emits: watch?v= (v anywhere in the query), youtu.be/, /embed/, /shorts/,
|
|
23
|
-
* /live/, /v/, m. and music. subdomains, and youtube-nocookie.com/embed/.
|
|
24
|
-
* Returns null for non-YouTube URLs.
|
|
25
|
-
*/
|
|
26
|
-
export function parseYouTubeUrl(rawUrl: string | undefined | null): ParsedYouTubeUrl | null {
|
|
27
|
-
if (!rawUrl) return null;
|
|
28
|
-
let url: URL;
|
|
29
|
-
try {
|
|
30
|
-
url = new URL(rawUrl, 'https://www.youtube.com');
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let videoId = '';
|
|
36
|
-
if (YOUTU_BE_RE.test(url.hostname)) {
|
|
37
|
-
videoId = url.pathname.split('/').filter(Boolean)[0] || '';
|
|
38
|
-
} else if (YOUTUBE_HOST_RE.test(url.hostname)) {
|
|
39
|
-
const segments = url.pathname.split('/').filter(Boolean);
|
|
40
|
-
const head = segments[0] || '';
|
|
41
|
-
if (head === 'watch') {
|
|
42
|
-
videoId = url.searchParams.get('v') || '';
|
|
43
|
-
} else if (head === 'embed' || head === 'shorts' || head === 'live' || head === 'v') {
|
|
44
|
-
videoId = segments[1] || '';
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (!VIDEO_ID_RE.test(videoId)) return null;
|
|
48
|
-
|
|
49
|
-
const params = new URLSearchParams(url.search);
|
|
50
|
-
params.delete('v');
|
|
51
|
-
params.delete('si'); // share-tracking token, not needed for playback
|
|
52
|
-
// Never auto-start on load: an autoplaying frame boots the player (and its
|
|
53
|
-
// cookie writes) during a Lighthouse run. The facade re-adds autoplay=1 only
|
|
54
|
-
// after a real user click.
|
|
55
|
-
params.delete('autoplay');
|
|
56
|
-
return { videoId, params };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function buildNoCookieEmbedUrl(
|
|
60
|
-
videoId: string,
|
|
61
|
-
params?: URLSearchParams,
|
|
62
|
-
opts?: { autoplay?: boolean }
|
|
63
|
-
): string {
|
|
64
|
-
const search = new URLSearchParams(params ? params.toString() : '');
|
|
65
|
-
if (opts?.autoplay) search.set('autoplay', '1');
|
|
66
|
-
const qs = search.toString();
|
|
67
|
-
return `https://www.youtube-nocookie.com/embed/${videoId}${qs ? `?${qs}` : ''}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Last-resort string rewrite for raw-HTML paths that cannot be parsed into nodes. */
|
|
71
|
-
export function rewriteYouTubeHostsInHtml(html: string): string {
|
|
72
|
-
if (!html) return html;
|
|
73
|
-
return html.replace(
|
|
74
|
-
/(?:https?:)?\/\/(?:www\.|m\.|music\.)?youtube\.com\/embed\//gi,
|
|
75
|
-
'https://www.youtube-nocookie.com/embed/'
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export type YouTubePosterQuality = 'maxres' | 'hq';
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Intrinsic size of each poster variant. maxresdefault is 16:9 but is not
|
|
83
|
-
* uploaded for every video; hqdefault always exists and is 4:3 (the frame is
|
|
84
|
-
* letterboxed inside it), which is why the facade object-covers the poster.
|
|
85
|
-
*/
|
|
86
|
-
export const YOUTUBE_POSTER_DIMENSIONS: Record<
|
|
87
|
-
YouTubePosterQuality,
|
|
88
|
-
{ width: number; height: number }
|
|
89
|
-
> = {
|
|
90
|
-
maxres: { width: 1280, height: 720 },
|
|
91
|
-
hq: { width: 480, height: 360 },
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export function youTubePosterUrl(
|
|
95
|
-
videoId: string,
|
|
96
|
-
quality: YouTubePosterQuality = 'maxres'
|
|
97
|
-
): string {
|
|
98
|
-
return `https://i.ytimg.com/vi/${videoId}/${quality === 'maxres' ? 'maxresdefault' : 'hqdefault'}.jpg`;
|
|
99
|
-
}
|
|
1
|
+
// Shared YouTube URL normalization.
|
|
2
|
+
//
|
|
3
|
+
// No public render path may emit a www.youtube.com iframe: the player writes
|
|
4
|
+
// cookies via document.cookie on load (TESTCOOKIESENABLED, LAST_RESULT_ENTRY_KEY,
|
|
5
|
+
// ExcludeSameSiteUnspecifiedTreatedAsLax) which Chrome logs as a DevTools "Cookie"
|
|
6
|
+
// issue and Lighthouse fails through the `inspector-issues` audit (Best Practices
|
|
7
|
+
// 26/27 = 96). Switching to youtube-nocookie.com does NOT stop those writes --
|
|
8
|
+
// only not loading the player does. See components/media/YouTubeFacade.tsx.
|
|
9
|
+
|
|
10
|
+
const YOUTUBE_HOST_RE = /^(?:www\.|m\.|music\.)?(?:youtube\.com|youtube-nocookie\.com)$/i;
|
|
11
|
+
const YOUTU_BE_RE = /^(?:www\.)?youtu\.be$/i;
|
|
12
|
+
const VIDEO_ID_RE = /^[A-Za-z0-9_-]{6,20}$/;
|
|
13
|
+
|
|
14
|
+
export interface ParsedYouTubeUrl {
|
|
15
|
+
videoId: string;
|
|
16
|
+
/** Extra player params carried over from the source URL (v/si/autoplay stripped). */
|
|
17
|
+
params: URLSearchParams;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Accepts every shape an author can paste or that YouTube's Share > Embed panel
|
|
22
|
+
* emits: watch?v= (v anywhere in the query), youtu.be/, /embed/, /shorts/,
|
|
23
|
+
* /live/, /v/, m. and music. subdomains, and youtube-nocookie.com/embed/.
|
|
24
|
+
* Returns null for non-YouTube URLs.
|
|
25
|
+
*/
|
|
26
|
+
export function parseYouTubeUrl(rawUrl: string | undefined | null): ParsedYouTubeUrl | null {
|
|
27
|
+
if (!rawUrl) return null;
|
|
28
|
+
let url: URL;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(rawUrl, 'https://www.youtube.com');
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let videoId = '';
|
|
36
|
+
if (YOUTU_BE_RE.test(url.hostname)) {
|
|
37
|
+
videoId = url.pathname.split('/').filter(Boolean)[0] || '';
|
|
38
|
+
} else if (YOUTUBE_HOST_RE.test(url.hostname)) {
|
|
39
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
40
|
+
const head = segments[0] || '';
|
|
41
|
+
if (head === 'watch') {
|
|
42
|
+
videoId = url.searchParams.get('v') || '';
|
|
43
|
+
} else if (head === 'embed' || head === 'shorts' || head === 'live' || head === 'v') {
|
|
44
|
+
videoId = segments[1] || '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!VIDEO_ID_RE.test(videoId)) return null;
|
|
48
|
+
|
|
49
|
+
const params = new URLSearchParams(url.search);
|
|
50
|
+
params.delete('v');
|
|
51
|
+
params.delete('si'); // share-tracking token, not needed for playback
|
|
52
|
+
// Never auto-start on load: an autoplaying frame boots the player (and its
|
|
53
|
+
// cookie writes) during a Lighthouse run. The facade re-adds autoplay=1 only
|
|
54
|
+
// after a real user click.
|
|
55
|
+
params.delete('autoplay');
|
|
56
|
+
return { videoId, params };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildNoCookieEmbedUrl(
|
|
60
|
+
videoId: string,
|
|
61
|
+
params?: URLSearchParams,
|
|
62
|
+
opts?: { autoplay?: boolean }
|
|
63
|
+
): string {
|
|
64
|
+
const search = new URLSearchParams(params ? params.toString() : '');
|
|
65
|
+
if (opts?.autoplay) search.set('autoplay', '1');
|
|
66
|
+
const qs = search.toString();
|
|
67
|
+
return `https://www.youtube-nocookie.com/embed/${videoId}${qs ? `?${qs}` : ''}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Last-resort string rewrite for raw-HTML paths that cannot be parsed into nodes. */
|
|
71
|
+
export function rewriteYouTubeHostsInHtml(html: string): string {
|
|
72
|
+
if (!html) return html;
|
|
73
|
+
return html.replace(
|
|
74
|
+
/(?:https?:)?\/\/(?:www\.|m\.|music\.)?youtube\.com\/embed\//gi,
|
|
75
|
+
'https://www.youtube-nocookie.com/embed/'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type YouTubePosterQuality = 'maxres' | 'hq';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Intrinsic size of each poster variant. maxresdefault is 16:9 but is not
|
|
83
|
+
* uploaded for every video; hqdefault always exists and is 4:3 (the frame is
|
|
84
|
+
* letterboxed inside it), which is why the facade object-covers the poster.
|
|
85
|
+
*/
|
|
86
|
+
export const YOUTUBE_POSTER_DIMENSIONS: Record<
|
|
87
|
+
YouTubePosterQuality,
|
|
88
|
+
{ width: number; height: number }
|
|
89
|
+
> = {
|
|
90
|
+
maxres: { width: 1280, height: 720 },
|
|
91
|
+
hq: { width: 480, height: 360 },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function youTubePosterUrl(
|
|
95
|
+
videoId: string,
|
|
96
|
+
quality: YouTubePosterQuality = 'maxres'
|
|
97
|
+
): string {
|
|
98
|
+
return `https://i.ytimg.com/vi/${videoId}/${quality === 'maxres' ? 'maxresdefault' : 'hqdefault'}.jpg`;
|
|
99
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
'use server';
|
|
2
|
-
|
|
3
|
-
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
-
import { revalidatePath } from 'next/cache';
|
|
5
|
-
|
|
6
|
-
/** Persist the onboarding dismiss flag in the `onboarding_state` row (read-merge). */
|
|
7
|
-
export async function setOnboardingDismissed(dismissed: boolean) {
|
|
8
|
-
const supabase = createClient();
|
|
9
|
-
|
|
10
|
-
const { data: existing } = await supabase
|
|
11
|
-
.from('site_settings')
|
|
12
|
-
.select('value')
|
|
13
|
-
.eq('key', 'onboarding_state')
|
|
14
|
-
.maybeSingle();
|
|
15
|
-
|
|
16
|
-
const current =
|
|
17
|
-
existing?.value && typeof existing.value === 'object' && !Array.isArray(existing.value)
|
|
18
|
-
? (existing.value as Record<string, unknown>)
|
|
19
|
-
: {};
|
|
20
|
-
|
|
21
|
-
const { error } = await supabase
|
|
22
|
-
.from('site_settings')
|
|
23
|
-
.upsert({ key: 'onboarding_state', value: { ...current, dismissed } });
|
|
24
|
-
|
|
25
|
-
if (error) {
|
|
26
|
-
console.error('Error updating onboarding state:', error.message);
|
|
27
|
-
throw new Error('Failed to update onboarding state.');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
revalidatePath('/cms/dashboard');
|
|
31
|
-
}
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
+
import { revalidatePath } from 'next/cache';
|
|
5
|
+
|
|
6
|
+
/** Persist the onboarding dismiss flag in the `onboarding_state` row (read-merge). */
|
|
7
|
+
export async function setOnboardingDismissed(dismissed: boolean) {
|
|
8
|
+
const supabase = createClient();
|
|
9
|
+
|
|
10
|
+
const { data: existing } = await supabase
|
|
11
|
+
.from('site_settings')
|
|
12
|
+
.select('value')
|
|
13
|
+
.eq('key', 'onboarding_state')
|
|
14
|
+
.maybeSingle();
|
|
15
|
+
|
|
16
|
+
const current =
|
|
17
|
+
existing?.value && typeof existing.value === 'object' && !Array.isArray(existing.value)
|
|
18
|
+
? (existing.value as Record<string, unknown>)
|
|
19
|
+
: {};
|
|
20
|
+
|
|
21
|
+
const { error } = await supabase
|
|
22
|
+
.from('site_settings')
|
|
23
|
+
.upsert({ key: 'onboarding_state', value: { ...current, dismissed } });
|
|
24
|
+
|
|
25
|
+
if (error) {
|
|
26
|
+
console.error('Error updating onboarding state:', error.message);
|
|
27
|
+
throw new Error('Failed to update onboarding state.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
revalidatePath('/cms/dashboard');
|
|
31
|
+
}
|