create-nextblock 0.16.3 → 0.17.0
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/CLAUDE.md +22 -0
- 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/package.json +1 -1
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/CLAUDE.md +26 -0
- 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/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- 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/checkout/freemius/sync/route.ts +29 -29
- 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 +270 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10388 -9315
- 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/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/article/[slug]/PostClientContent.tsx +442 -441
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -40
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
- 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/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- 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/components/DashboardComponents.tsx +200 -200
- 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/pages/page.tsx +273 -273
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +19 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +14 -2
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- 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/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/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -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/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/page.tsx +33 -33
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- 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/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/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/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- 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/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- 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/ClientTextBlockRenderer.tsx +11 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- 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/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/visual-editing/DeferredVisualEditing.tsx +21 -21
- 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/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- 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/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
- 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/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -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/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/seo/page-audit-context.tsx +3 -3
- package/templates/nextblock-template/lib/seo/page-document.test.ts +34 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +3 -462
- package/templates/nextblock-template/lib/seo/robots-txt.ts +5 -4
- 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 +50 -10
- 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/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- 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/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 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
- package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- 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/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
|
@@ -1,276 +1,276 @@
|
|
|
1
|
-
'use server';
|
|
2
|
-
|
|
3
|
-
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
-
import { revalidatePath, updateTag } from 'next/cache';
|
|
5
|
-
import { z } from '../../../../lib/zod-config';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
getOrderedTranslationLanguageCodes,
|
|
9
|
-
prepareTranslationCsvImport,
|
|
10
|
-
type TranslationWorkspaceItem,
|
|
11
|
-
} from '@nextblock-cms/utils';
|
|
12
|
-
|
|
13
|
-
const translationSchema = z.object({
|
|
14
|
-
key: z.string().min(1, 'Key is required.'),
|
|
15
|
-
en: z.string().min(1, 'English translation is required.'),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const EXTRA_TRANSLATIONS_PATH = '/cms/settings/extra-translations';
|
|
19
|
-
const PUBLIC_TRANSLATIONS_CACHE_TAG = 'public-layout-translations';
|
|
20
|
-
|
|
21
|
-
type TranslationMutationState = {
|
|
22
|
-
success?: boolean;
|
|
23
|
-
translation?: TranslationWorkspaceItem;
|
|
24
|
-
error?: string;
|
|
25
|
-
errors?: Record<string, string[] | undefined>;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
async function verifyAdmin(supabase: ReturnType<typeof createClient>) {
|
|
29
|
-
const {
|
|
30
|
-
data: { user },
|
|
31
|
-
} = await supabase.auth.getUser();
|
|
32
|
-
|
|
33
|
-
if (!user) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const { data: profile } = await supabase
|
|
38
|
-
.from('profiles')
|
|
39
|
-
.select('role')
|
|
40
|
-
.eq('id', user.id)
|
|
41
|
-
.single();
|
|
42
|
-
|
|
43
|
-
return profile?.role === 'ADMIN';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function getConfiguredLanguageCodes() {
|
|
47
|
-
const supabase = createClient();
|
|
48
|
-
const { data, error } = await supabase
|
|
49
|
-
.from('languages')
|
|
50
|
-
.select('code, name')
|
|
51
|
-
.order('is_default', { ascending: false })
|
|
52
|
-
.order('name', { ascending: true });
|
|
53
|
-
|
|
54
|
-
if (error) {
|
|
55
|
-
throw new Error(`Failed to fetch languages: ${error.message}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return getOrderedTranslationLanguageCodes(data ?? []);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function revalidateTranslationViews() {
|
|
62
|
-
revalidatePath(EXTRA_TRANSLATIONS_PATH);
|
|
63
|
-
updateTag(PUBLIC_TRANSLATIONS_CACHE_TAG);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function createTranslation(
|
|
67
|
-
prevState: TranslationMutationState | null,
|
|
68
|
-
formData: FormData
|
|
69
|
-
): Promise<TranslationMutationState> {
|
|
70
|
-
void prevState;
|
|
71
|
-
|
|
72
|
-
const supabase = createClient();
|
|
73
|
-
|
|
74
|
-
if (!(await verifyAdmin(supabase))) {
|
|
75
|
-
return { error: 'Unauthorized: Admin role required.' };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const data = {
|
|
79
|
-
key: formData.get('key') as string,
|
|
80
|
-
en: formData.get('en') as string,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const validatedFields = translationSchema.safeParse(data);
|
|
84
|
-
|
|
85
|
-
if (!validatedFields.success) {
|
|
86
|
-
return {
|
|
87
|
-
errors: validatedFields.error.flatten().fieldErrors,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const { data: insertedTranslation, error } = await supabase
|
|
92
|
-
.from('translations')
|
|
93
|
-
.insert({
|
|
94
|
-
key: validatedFields.data.key,
|
|
95
|
-
translations: { en: validatedFields.data.en },
|
|
96
|
-
})
|
|
97
|
-
.select('key, translations, created_at, updated_at')
|
|
98
|
-
.single();
|
|
99
|
-
|
|
100
|
-
if (error || !insertedTranslation) {
|
|
101
|
-
return {
|
|
102
|
-
error: error?.message ?? 'Failed to create translation.',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
revalidateTranslationViews();
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
success: true,
|
|
110
|
-
translation: insertedTranslation as TranslationWorkspaceItem,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function getTranslations() {
|
|
115
|
-
const supabase = createClient();
|
|
116
|
-
const { data, error } = await supabase
|
|
117
|
-
.from('translations')
|
|
118
|
-
.select('key, translations, created_at, updated_at')
|
|
119
|
-
.order('key');
|
|
120
|
-
|
|
121
|
-
if (error) {
|
|
122
|
-
console.error('Error fetching translations:', error);
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return (data ?? []) as TranslationWorkspaceItem[];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export async function updateTranslation(
|
|
130
|
-
prevState: TranslationMutationState | null,
|
|
131
|
-
formData: FormData
|
|
132
|
-
): Promise<TranslationMutationState> {
|
|
133
|
-
void prevState;
|
|
134
|
-
|
|
135
|
-
const supabase = createClient();
|
|
136
|
-
|
|
137
|
-
if (!(await verifyAdmin(supabase))) {
|
|
138
|
-
return { error: 'Unauthorized: Admin role required.' };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const data = Object.fromEntries(formData.entries());
|
|
142
|
-
const key = data.key as string;
|
|
143
|
-
|
|
144
|
-
if (!key) {
|
|
145
|
-
return {
|
|
146
|
-
error: 'Translation key is required.',
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const languageEntries = Object.entries(data).filter(
|
|
151
|
-
([fieldKey]) => fieldKey !== 'key'
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
if (languageEntries.length === 0) {
|
|
155
|
-
return {
|
|
156
|
-
error: 'No translation values were provided.',
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const { data: existingData, error: fetchError } = await supabase
|
|
161
|
-
.from('translations')
|
|
162
|
-
.select('key, translations, created_at, updated_at')
|
|
163
|
-
.eq('key', key)
|
|
164
|
-
.single();
|
|
165
|
-
|
|
166
|
-
if (fetchError || !existingData) {
|
|
167
|
-
return {
|
|
168
|
-
error: fetchError
|
|
169
|
-
? `Failed to fetch existing translation: ${fetchError.message}`
|
|
170
|
-
: `Translation with key "${key}" was not found.`,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const nextTranslations = {
|
|
175
|
-
...((existingData.translations as Record<string, string>) ?? {}),
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
for (const [languageCode, value] of languageEntries) {
|
|
179
|
-
nextTranslations[languageCode] = value as string;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const { data: updatedTranslation, error } = await supabase
|
|
183
|
-
.from('translations')
|
|
184
|
-
.update({ translations: nextTranslations })
|
|
185
|
-
.eq('key', key)
|
|
186
|
-
.select('key, translations, created_at, updated_at')
|
|
187
|
-
.single();
|
|
188
|
-
|
|
189
|
-
if (error || !updatedTranslation) {
|
|
190
|
-
return {
|
|
191
|
-
error:
|
|
192
|
-
error?.message ??
|
|
193
|
-
`Translation with key "${key}" could not be updated.`,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
revalidateTranslationViews();
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
success: true,
|
|
201
|
-
translation: updatedTranslation as TranslationWorkspaceItem,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export async function importTranslationsCsvAction(csvText: string) {
|
|
206
|
-
try {
|
|
207
|
-
const supabase = createClient();
|
|
208
|
-
|
|
209
|
-
if (!(await verifyAdmin(supabase))) {
|
|
210
|
-
return {
|
|
211
|
-
success: false as const,
|
|
212
|
-
createdCount: 0,
|
|
213
|
-
updatedCount: 0,
|
|
214
|
-
skippedCount: 0,
|
|
215
|
-
error: 'Unauthorized: Admin role required.',
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const [existingTranslations, languageCodes] = await Promise.all([
|
|
220
|
-
getTranslations(),
|
|
221
|
-
getConfiguredLanguageCodes(),
|
|
222
|
-
]);
|
|
223
|
-
const importResult = prepareTranslationCsvImport({
|
|
224
|
-
csvText,
|
|
225
|
-
existingItems: existingTranslations,
|
|
226
|
-
languageCodes,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (!importResult.success) {
|
|
230
|
-
return importResult;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const rowsToUpsert = [
|
|
234
|
-
...importResult.createdRows,
|
|
235
|
-
...importResult.updatedRows,
|
|
236
|
-
].map((row) => ({
|
|
237
|
-
key: row.key,
|
|
238
|
-
translations: row.translations,
|
|
239
|
-
}));
|
|
240
|
-
|
|
241
|
-
if (rowsToUpsert.length > 0) {
|
|
242
|
-
const { error } = await supabase
|
|
243
|
-
.from('translations')
|
|
244
|
-
.upsert(rowsToUpsert, { onConflict: 'key' });
|
|
245
|
-
|
|
246
|
-
if (error) {
|
|
247
|
-
return {
|
|
248
|
-
success: false as const,
|
|
249
|
-
createdCount: 0,
|
|
250
|
-
updatedCount: 0,
|
|
251
|
-
skippedCount: importResult.skippedCount,
|
|
252
|
-
error: error.message,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
revalidateTranslationViews();
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
success: true as const,
|
|
261
|
-
createdCount: importResult.createdCount,
|
|
262
|
-
updatedCount: importResult.updatedCount,
|
|
263
|
-
skippedCount: importResult.skippedCount,
|
|
264
|
-
translations: await getTranslations(),
|
|
265
|
-
};
|
|
266
|
-
} catch (error) {
|
|
267
|
-
return {
|
|
268
|
-
success: false as const,
|
|
269
|
-
createdCount: 0,
|
|
270
|
-
updatedCount: 0,
|
|
271
|
-
skippedCount: 0,
|
|
272
|
-
error:
|
|
273
|
-
error instanceof Error ? error.message : 'Failed to import translation CSV.',
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
}
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
+
import { revalidatePath, updateTag } from 'next/cache';
|
|
5
|
+
import { z } from '../../../../lib/zod-config';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getOrderedTranslationLanguageCodes,
|
|
9
|
+
prepareTranslationCsvImport,
|
|
10
|
+
type TranslationWorkspaceItem,
|
|
11
|
+
} from '@nextblock-cms/utils';
|
|
12
|
+
|
|
13
|
+
const translationSchema = z.object({
|
|
14
|
+
key: z.string().min(1, 'Key is required.'),
|
|
15
|
+
en: z.string().min(1, 'English translation is required.'),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const EXTRA_TRANSLATIONS_PATH = '/cms/settings/extra-translations';
|
|
19
|
+
const PUBLIC_TRANSLATIONS_CACHE_TAG = 'public-layout-translations';
|
|
20
|
+
|
|
21
|
+
type TranslationMutationState = {
|
|
22
|
+
success?: boolean;
|
|
23
|
+
translation?: TranslationWorkspaceItem;
|
|
24
|
+
error?: string;
|
|
25
|
+
errors?: Record<string, string[] | undefined>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function verifyAdmin(supabase: ReturnType<typeof createClient>) {
|
|
29
|
+
const {
|
|
30
|
+
data: { user },
|
|
31
|
+
} = await supabase.auth.getUser();
|
|
32
|
+
|
|
33
|
+
if (!user) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { data: profile } = await supabase
|
|
38
|
+
.from('profiles')
|
|
39
|
+
.select('role')
|
|
40
|
+
.eq('id', user.id)
|
|
41
|
+
.single();
|
|
42
|
+
|
|
43
|
+
return profile?.role === 'ADMIN';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getConfiguredLanguageCodes() {
|
|
47
|
+
const supabase = createClient();
|
|
48
|
+
const { data, error } = await supabase
|
|
49
|
+
.from('languages')
|
|
50
|
+
.select('code, name')
|
|
51
|
+
.order('is_default', { ascending: false })
|
|
52
|
+
.order('name', { ascending: true });
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
throw new Error(`Failed to fetch languages: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return getOrderedTranslationLanguageCodes(data ?? []);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function revalidateTranslationViews() {
|
|
62
|
+
revalidatePath(EXTRA_TRANSLATIONS_PATH);
|
|
63
|
+
updateTag(PUBLIC_TRANSLATIONS_CACHE_TAG);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function createTranslation(
|
|
67
|
+
prevState: TranslationMutationState | null,
|
|
68
|
+
formData: FormData
|
|
69
|
+
): Promise<TranslationMutationState> {
|
|
70
|
+
void prevState;
|
|
71
|
+
|
|
72
|
+
const supabase = createClient();
|
|
73
|
+
|
|
74
|
+
if (!(await verifyAdmin(supabase))) {
|
|
75
|
+
return { error: 'Unauthorized: Admin role required.' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const data = {
|
|
79
|
+
key: formData.get('key') as string,
|
|
80
|
+
en: formData.get('en') as string,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const validatedFields = translationSchema.safeParse(data);
|
|
84
|
+
|
|
85
|
+
if (!validatedFields.success) {
|
|
86
|
+
return {
|
|
87
|
+
errors: validatedFields.error.flatten().fieldErrors,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { data: insertedTranslation, error } = await supabase
|
|
92
|
+
.from('translations')
|
|
93
|
+
.insert({
|
|
94
|
+
key: validatedFields.data.key,
|
|
95
|
+
translations: { en: validatedFields.data.en },
|
|
96
|
+
})
|
|
97
|
+
.select('key, translations, created_at, updated_at')
|
|
98
|
+
.single();
|
|
99
|
+
|
|
100
|
+
if (error || !insertedTranslation) {
|
|
101
|
+
return {
|
|
102
|
+
error: error?.message ?? 'Failed to create translation.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
revalidateTranslationViews();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
translation: insertedTranslation as TranslationWorkspaceItem,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getTranslations() {
|
|
115
|
+
const supabase = createClient();
|
|
116
|
+
const { data, error } = await supabase
|
|
117
|
+
.from('translations')
|
|
118
|
+
.select('key, translations, created_at, updated_at')
|
|
119
|
+
.order('key');
|
|
120
|
+
|
|
121
|
+
if (error) {
|
|
122
|
+
console.error('Error fetching translations:', error);
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (data ?? []) as TranslationWorkspaceItem[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function updateTranslation(
|
|
130
|
+
prevState: TranslationMutationState | null,
|
|
131
|
+
formData: FormData
|
|
132
|
+
): Promise<TranslationMutationState> {
|
|
133
|
+
void prevState;
|
|
134
|
+
|
|
135
|
+
const supabase = createClient();
|
|
136
|
+
|
|
137
|
+
if (!(await verifyAdmin(supabase))) {
|
|
138
|
+
return { error: 'Unauthorized: Admin role required.' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const data = Object.fromEntries(formData.entries());
|
|
142
|
+
const key = data.key as string;
|
|
143
|
+
|
|
144
|
+
if (!key) {
|
|
145
|
+
return {
|
|
146
|
+
error: 'Translation key is required.',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const languageEntries = Object.entries(data).filter(
|
|
151
|
+
([fieldKey]) => fieldKey !== 'key'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (languageEntries.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
error: 'No translation values were provided.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { data: existingData, error: fetchError } = await supabase
|
|
161
|
+
.from('translations')
|
|
162
|
+
.select('key, translations, created_at, updated_at')
|
|
163
|
+
.eq('key', key)
|
|
164
|
+
.single();
|
|
165
|
+
|
|
166
|
+
if (fetchError || !existingData) {
|
|
167
|
+
return {
|
|
168
|
+
error: fetchError
|
|
169
|
+
? `Failed to fetch existing translation: ${fetchError.message}`
|
|
170
|
+
: `Translation with key "${key}" was not found.`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const nextTranslations = {
|
|
175
|
+
...((existingData.translations as Record<string, string>) ?? {}),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (const [languageCode, value] of languageEntries) {
|
|
179
|
+
nextTranslations[languageCode] = value as string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { data: updatedTranslation, error } = await supabase
|
|
183
|
+
.from('translations')
|
|
184
|
+
.update({ translations: nextTranslations })
|
|
185
|
+
.eq('key', key)
|
|
186
|
+
.select('key, translations, created_at, updated_at')
|
|
187
|
+
.single();
|
|
188
|
+
|
|
189
|
+
if (error || !updatedTranslation) {
|
|
190
|
+
return {
|
|
191
|
+
error:
|
|
192
|
+
error?.message ??
|
|
193
|
+
`Translation with key "${key}" could not be updated.`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
revalidateTranslationViews();
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
translation: updatedTranslation as TranslationWorkspaceItem,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function importTranslationsCsvAction(csvText: string) {
|
|
206
|
+
try {
|
|
207
|
+
const supabase = createClient();
|
|
208
|
+
|
|
209
|
+
if (!(await verifyAdmin(supabase))) {
|
|
210
|
+
return {
|
|
211
|
+
success: false as const,
|
|
212
|
+
createdCount: 0,
|
|
213
|
+
updatedCount: 0,
|
|
214
|
+
skippedCount: 0,
|
|
215
|
+
error: 'Unauthorized: Admin role required.',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const [existingTranslations, languageCodes] = await Promise.all([
|
|
220
|
+
getTranslations(),
|
|
221
|
+
getConfiguredLanguageCodes(),
|
|
222
|
+
]);
|
|
223
|
+
const importResult = prepareTranslationCsvImport({
|
|
224
|
+
csvText,
|
|
225
|
+
existingItems: existingTranslations,
|
|
226
|
+
languageCodes,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!importResult.success) {
|
|
230
|
+
return importResult;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const rowsToUpsert = [
|
|
234
|
+
...importResult.createdRows,
|
|
235
|
+
...importResult.updatedRows,
|
|
236
|
+
].map((row) => ({
|
|
237
|
+
key: row.key,
|
|
238
|
+
translations: row.translations,
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
if (rowsToUpsert.length > 0) {
|
|
242
|
+
const { error } = await supabase
|
|
243
|
+
.from('translations')
|
|
244
|
+
.upsert(rowsToUpsert, { onConflict: 'key' });
|
|
245
|
+
|
|
246
|
+
if (error) {
|
|
247
|
+
return {
|
|
248
|
+
success: false as const,
|
|
249
|
+
createdCount: 0,
|
|
250
|
+
updatedCount: 0,
|
|
251
|
+
skippedCount: importResult.skippedCount,
|
|
252
|
+
error: error.message,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
revalidateTranslationViews();
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
success: true as const,
|
|
261
|
+
createdCount: importResult.createdCount,
|
|
262
|
+
updatedCount: importResult.updatedCount,
|
|
263
|
+
skippedCount: importResult.skippedCount,
|
|
264
|
+
translations: await getTranslations(),
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return {
|
|
268
|
+
success: false as const,
|
|
269
|
+
createdCount: 0,
|
|
270
|
+
updatedCount: 0,
|
|
271
|
+
skippedCount: 0,
|
|
272
|
+
error:
|
|
273
|
+
error instanceof Error ? error.message : 'Failed to import translation CSV.',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -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 & 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 & 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
|
+
}
|
|
@@ -2,31 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
import { DropdownMenuItem } from "@nextblock-cms/ui";
|
|
4
4
|
import { Trash2 } from "lucide-react";
|
|
5
|
-
import { deleteLogo } from "../actions";
|
|
6
|
-
import { useTransition, useState } from 'react';
|
|
7
|
-
import { ConfirmationModal } from '../../../components/ConfirmationModal';
|
|
8
|
-
import { toast } from 'react-hot-toast';
|
|
9
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { deleteLogo } from "../actions";
|
|
6
|
+
import { useTransition, useState } from 'react';
|
|
7
|
+
import { ConfirmationModal } from '../../../components/ConfirmationModal';
|
|
8
|
+
import { toast } from 'react-hot-toast';
|
|
9
|
+
import { useRouter } from 'next/navigation';
|
|
10
10
|
|
|
11
11
|
interface DeleteLogoButtonProps {
|
|
12
12
|
logoId: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export default function DeleteLogoButton({ logoId }: DeleteLogoButtonProps) {
|
|
16
|
-
const router = useRouter();
|
|
17
|
-
const [isPending, startTransition] = useTransition();
|
|
18
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
15
|
+
export default function DeleteLogoButton({ logoId }: DeleteLogoButtonProps) {
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
const [isPending, startTransition] = useTransition();
|
|
18
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
19
19
|
|
|
20
20
|
const handleDeleteConfirm = () => {
|
|
21
21
|
startTransition(async () => {
|
|
22
22
|
const result = await deleteLogo(logoId);
|
|
23
23
|
if (result?.error) {
|
|
24
24
|
toast.error(`Error: ${result.error}`);
|
|
25
|
-
} else {
|
|
26
|
-
toast.success("Logo deleted successfully");
|
|
27
|
-
router.refresh();
|
|
28
|
-
}
|
|
29
|
-
setIsModalOpen(false);
|
|
25
|
+
} else {
|
|
26
|
+
toast.success("Logo deleted successfully");
|
|
27
|
+
router.refresh();
|
|
28
|
+
}
|
|
29
|
+
setIsModalOpen(false);
|
|
30
30
|
});
|
|
31
31
|
};
|
|
32
32
|
|
|
@@ -41,13 +41,13 @@ export default function DeleteLogoButton({ logoId }: DeleteLogoButtonProps) {
|
|
|
41
41
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
42
42
|
{isPending ? "Deleting..." : "Delete"}
|
|
43
43
|
</DropdownMenuItem>
|
|
44
|
-
<ConfirmationModal
|
|
45
|
-
isOpen={isModalOpen}
|
|
46
|
-
onClose={() => setIsModalOpen(false)}
|
|
47
|
-
onConfirm={handleDeleteConfirm}
|
|
48
|
-
title="Delete Brand Logo?"
|
|
49
|
-
description="This will permanently delete the logo. This action cannot be undone."
|
|
50
|
-
/>
|
|
44
|
+
<ConfirmationModal
|
|
45
|
+
isOpen={isModalOpen}
|
|
46
|
+
onClose={() => setIsModalOpen(false)}
|
|
47
|
+
onConfirm={handleDeleteConfirm}
|
|
48
|
+
title="Delete Brand Logo?"
|
|
49
|
+
description="This will permanently delete the logo. This action cannot be undone."
|
|
50
|
+
/>
|
|
51
51
|
</>
|
|
52
52
|
);
|
|
53
53
|
}
|