create-nextblock 0.0.1
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/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- package/templates/nextblock-template/tsconfig.json +62 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
+
import { revalidatePath } from 'next/cache';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const translationSchema = z.object({
|
|
8
|
+
key: z.string().min(1, 'Key is required.'),
|
|
9
|
+
en: z.string().min(1, 'English translation is required.'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export async function createTranslation(prevState: unknown, formData: FormData) {
|
|
13
|
+
const supabase = createClient();
|
|
14
|
+
const data = {
|
|
15
|
+
key: formData.get('key') as string,
|
|
16
|
+
en: formData.get('en') as string,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const validatedFields = translationSchema.safeParse({
|
|
20
|
+
key: data.key,
|
|
21
|
+
en: data.en,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!validatedFields.success) {
|
|
25
|
+
return {
|
|
26
|
+
errors: validatedFields.error.flatten().fieldErrors,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { error } = await supabase.from('translations').insert({
|
|
31
|
+
key: validatedFields.data.key,
|
|
32
|
+
translations: { en: validatedFields.data.en },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
return {
|
|
37
|
+
error: error.message,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
revalidatePath('/cms/settings/extra-translations');
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getTranslations() {
|
|
49
|
+
const supabase = createClient();
|
|
50
|
+
const { data, error } = await supabase.from('translations').select('key, translations, created_at, updated_at').order('key');
|
|
51
|
+
|
|
52
|
+
if (error) {
|
|
53
|
+
console.error('Error fetching translations:', error);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function updateTranslation(prevState: unknown, formData: FormData) {
|
|
61
|
+
const supabase = createClient();
|
|
62
|
+
const data = Object.fromEntries(formData as any);
|
|
63
|
+
const key = data.key as string;
|
|
64
|
+
|
|
65
|
+
if (!key) {
|
|
66
|
+
return {
|
|
67
|
+
error: 'Translation key is required',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// First, fetch the existing translation to get current translations
|
|
72
|
+
const { data: existingData, error: fetchError } = await supabase
|
|
73
|
+
.from('translations')
|
|
74
|
+
.select('translations')
|
|
75
|
+
.eq('key', key)
|
|
76
|
+
.single();
|
|
77
|
+
|
|
78
|
+
if (fetchError) {
|
|
79
|
+
return {
|
|
80
|
+
error: `Failed to fetch existing translation: ${fetchError.message}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge new translations with existing ones
|
|
85
|
+
const existingTranslations = (existingData?.translations as Record<string, string>) || {};
|
|
86
|
+
const newTranslations: { [key: string]: string } = { ...existingTranslations };
|
|
87
|
+
|
|
88
|
+
for (const [formKey, value] of Object.entries(data)) {
|
|
89
|
+
if (formKey !== 'key') {
|
|
90
|
+
newTranslations[formKey] = value as string;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { data: updateResult, error } = await supabase
|
|
95
|
+
.from('translations')
|
|
96
|
+
.update({ translations: newTranslations })
|
|
97
|
+
.eq('key', key)
|
|
98
|
+
.select();
|
|
99
|
+
|
|
100
|
+
if (error) {
|
|
101
|
+
return {
|
|
102
|
+
error: error.message,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!updateResult || updateResult.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
error: `Translation with key "${key}" not found or could not be updated`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
revalidatePath('/cms/settings/extra-translations');
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useTransition } from 'react';
|
|
4
|
+
import { useActionState } from 'react';
|
|
5
|
+
import { getTranslations, createTranslation, updateTranslation } from './actions';
|
|
6
|
+
import { getLanguages } from '@/app/cms/settings/languages/actions';
|
|
7
|
+
import { Button } from '@nextblock-cms/ui';
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
} from '@nextblock-cms/ui';
|
|
16
|
+
import { Input } from '@nextblock-cms/ui';
|
|
17
|
+
import { Label } from '@nextblock-cms/ui';
|
|
18
|
+
import {
|
|
19
|
+
Table,
|
|
20
|
+
TableBody,
|
|
21
|
+
TableCell,
|
|
22
|
+
TableHead,
|
|
23
|
+
TableHeader,
|
|
24
|
+
TableRow,
|
|
25
|
+
} from '@nextblock-cms/ui';
|
|
26
|
+
import { SubmitButton } from '@/components/submit-button';
|
|
27
|
+
|
|
28
|
+
type Translation = Awaited<ReturnType<typeof getTranslations>>[number];
|
|
29
|
+
type Language = NonNullable<Awaited<ReturnType<typeof getLanguages>>['data']>[number];
|
|
30
|
+
|
|
31
|
+
export default function ExtraTranslationsPage() {
|
|
32
|
+
const [translations, setTranslations] = useState<Translation[]>([]);
|
|
33
|
+
const [languages, setLanguages] = useState<Language[]>([]);
|
|
34
|
+
const [isPending, startTransition] = useTransition();
|
|
35
|
+
|
|
36
|
+
const fetchData = () => {
|
|
37
|
+
startTransition(async () => {
|
|
38
|
+
const translationsData = await getTranslations();
|
|
39
|
+
const { data: languagesData } = await getLanguages();
|
|
40
|
+
setTranslations(translationsData);
|
|
41
|
+
if (languagesData) {
|
|
42
|
+
setLanguages(languagesData);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
fetchData();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
if (isPending && translations.length === 0) {
|
|
52
|
+
return <div>Loading...</div>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="p-6">
|
|
57
|
+
<div className="flex justify-between items-center mb-6">
|
|
58
|
+
<h1 className="text-2xl font-bold">Extra Translations</h1>
|
|
59
|
+
<CreateTranslationForm onSuccess={fetchData} />
|
|
60
|
+
</div>
|
|
61
|
+
<TranslationsTable translations={translations} languages={languages} onSuccess={fetchData} />
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function CreateTranslationForm({ onSuccess }: { onSuccess: () => void }) {
|
|
67
|
+
const [state, formAction] = useActionState(createTranslation, null);
|
|
68
|
+
const [open, setOpen] = useState(false);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (state?.success) {
|
|
72
|
+
setOpen(false);
|
|
73
|
+
onSuccess();
|
|
74
|
+
}
|
|
75
|
+
}, [state, onSuccess]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
79
|
+
<DialogTrigger asChild>
|
|
80
|
+
<Button>Create New Translation</Button>
|
|
81
|
+
</DialogTrigger>
|
|
82
|
+
<DialogContent>
|
|
83
|
+
<DialogHeader>
|
|
84
|
+
<DialogTitle>Create New Translation</DialogTitle>
|
|
85
|
+
</DialogHeader>
|
|
86
|
+
<form action={formAction} className="space-y-4">
|
|
87
|
+
<div>
|
|
88
|
+
<Label htmlFor="key">Key</Label>
|
|
89
|
+
<Input id="key" name="key" placeholder="e.g., sign_in_button" required />
|
|
90
|
+
{state?.errors?.key && <p className="text-red-500 text-sm mt-1">{state.errors.key[0]}</p>}
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<Label htmlFor="en">English</Label>
|
|
94
|
+
<Input id="en" name="en" placeholder="e.g., Sign In" required />
|
|
95
|
+
{state?.errors?.en && <p className="text-red-500 text-sm mt-1">{state.errors.en[0]}</p>}
|
|
96
|
+
</div>
|
|
97
|
+
{state?.error && <p className="text-red-500 text-sm">{state.error}</p>}
|
|
98
|
+
<DialogFooter>
|
|
99
|
+
<SubmitButton>Create</SubmitButton>
|
|
100
|
+
</DialogFooter>
|
|
101
|
+
</form>
|
|
102
|
+
</DialogContent>
|
|
103
|
+
</Dialog>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type TranslationsTableProps = {
|
|
108
|
+
translations: Translation[];
|
|
109
|
+
languages: Language[];
|
|
110
|
+
onSuccess: () => void;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function TranslationsTable({ translations, languages, onSuccess }: TranslationsTableProps) {
|
|
114
|
+
return (
|
|
115
|
+
<div className="border rounded-lg">
|
|
116
|
+
<Table>
|
|
117
|
+
<TableHeader>
|
|
118
|
+
<TableRow>
|
|
119
|
+
<TableHead>Key</TableHead>
|
|
120
|
+
{languages.map((lang) => (
|
|
121
|
+
<TableHead key={lang.code}>{lang.name}</TableHead>
|
|
122
|
+
))}
|
|
123
|
+
<TableHead className="text-right w-[100px]">Actions</TableHead>
|
|
124
|
+
</TableRow>
|
|
125
|
+
</TableHeader>
|
|
126
|
+
<TableBody>
|
|
127
|
+
{translations.map((t) => (
|
|
128
|
+
<EditableTranslationRow key={t.key} translation={t} languages={languages} onSuccess={onSuccess} />
|
|
129
|
+
))}
|
|
130
|
+
</TableBody>
|
|
131
|
+
</Table>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type EditableRowProps = {
|
|
137
|
+
translation: Translation;
|
|
138
|
+
languages: Language[];
|
|
139
|
+
onSuccess: () => void;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function EditableTranslationRow({ translation, languages, onSuccess }: EditableRowProps) {
|
|
143
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
144
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
145
|
+
const [error, setError] = useState<string | null>(null);
|
|
146
|
+
const [formValues, setFormValues] = useState(() => translation.translations as Record<string, string>);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
setFormValues(translation.translations as Record<string, string>);
|
|
150
|
+
setIsDirty(false);
|
|
151
|
+
setError(null);
|
|
152
|
+
}, [translation]);
|
|
153
|
+
|
|
154
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
155
|
+
setIsDirty(true);
|
|
156
|
+
const { name, value } = e.target;
|
|
157
|
+
setFormValues(prev => ({ ...prev, [name]: value }));
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
|
|
163
|
+
if (isSubmitting || !isDirty) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setIsSubmitting(true);
|
|
168
|
+
setError(null);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const formData = new FormData();
|
|
172
|
+
formData.append('key', translation.key);
|
|
173
|
+
for (const [lang, value] of Object.entries(formValues)) {
|
|
174
|
+
formData.append(lang, value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await updateTranslation(null, formData);
|
|
178
|
+
|
|
179
|
+
if (result?.success) {
|
|
180
|
+
setIsDirty(false);
|
|
181
|
+
onSuccess();
|
|
182
|
+
} else if (result?.error) {
|
|
183
|
+
setError(result.error);
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error('Submit error:', err);
|
|
187
|
+
setError('An unexpected error occurred');
|
|
188
|
+
} finally {
|
|
189
|
+
setIsSubmitting(false);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<TableRow>
|
|
195
|
+
<TableCell className="font-medium">{translation.key}</TableCell>
|
|
196
|
+
{languages.map((lang) => (
|
|
197
|
+
<TableCell key={lang.code}>
|
|
198
|
+
<Input
|
|
199
|
+
name={lang.code}
|
|
200
|
+
value={formValues[lang.code] || ''}
|
|
201
|
+
onChange={handleInputChange}
|
|
202
|
+
className="w-full"
|
|
203
|
+
/>
|
|
204
|
+
</TableCell>
|
|
205
|
+
))}
|
|
206
|
+
<TableCell className="text-right">
|
|
207
|
+
<form onSubmit={handleSubmit}>
|
|
208
|
+
<Button type="submit" disabled={!isDirty || isSubmitting}>
|
|
209
|
+
{isSubmitting ? 'Saving...' : 'Save'}
|
|
210
|
+
</Button>
|
|
211
|
+
{error && <p className="text-red-500 text-xs mt-1">{error}</p>}
|
|
212
|
+
</form>
|
|
213
|
+
</TableCell>
|
|
214
|
+
</TableRow>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// app/cms/settings/languages/[id]/edit/page.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import LanguageForm from "../../components/LanguageForm";
|
|
5
|
+
import { updateLanguage } from "../../actions";
|
|
6
|
+
import type { Database } from "@nextblock-cms/db";
|
|
7
|
+
import { notFound } from "next/navigation";
|
|
8
|
+
|
|
9
|
+
type Language = Database['public']['Tables']['languages']['Row'];
|
|
10
|
+
import Link from "next/link";
|
|
11
|
+
import { Button } from "@nextblock-cms/ui";
|
|
12
|
+
import { ArrowLeft } from "lucide-react";
|
|
13
|
+
|
|
14
|
+
async function getLanguageData(id: number): Promise<Language | null> {
|
|
15
|
+
const supabase = createClient();
|
|
16
|
+
const { data, error } = await supabase
|
|
17
|
+
.from("languages")
|
|
18
|
+
.select("*")
|
|
19
|
+
.eq("id", id)
|
|
20
|
+
.single();
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
console.error("Error fetching language for edit:", error);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getAllLanguages(): Promise<Language[]> {
|
|
30
|
+
const supabase = createClient();
|
|
31
|
+
const { data, error } = await supabase.from("languages").select("*");
|
|
32
|
+
if (error) {
|
|
33
|
+
console.error("Error fetching all languages for edit page form:", error);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return data || [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function EditLanguagePage(props: { params: Promise<{ id: string }> }) {
|
|
40
|
+
const params = await props.params;
|
|
41
|
+
const languageId = parseInt(params.id, 10);
|
|
42
|
+
if (isNaN(languageId)) {
|
|
43
|
+
return notFound();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const [language, allLanguages] = await Promise.all([
|
|
47
|
+
getLanguageData(languageId),
|
|
48
|
+
getAllLanguages()
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if (!language) {
|
|
53
|
+
return notFound();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const updateLanguageWithId = updateLanguage.bind(null, languageId);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="max-w-xl mx-auto">
|
|
60
|
+
<div className="mb-6 flex items-center gap-3">
|
|
61
|
+
<Button variant="outline" size="icon" asChild>
|
|
62
|
+
<Link href="/cms/settings/languages">
|
|
63
|
+
<ArrowLeft className="h-4 w-4" />
|
|
64
|
+
</Link>
|
|
65
|
+
</Button>
|
|
66
|
+
<h1 className="text-2xl font-semibold">Edit Language: {language.name}</h1>
|
|
67
|
+
</div>
|
|
68
|
+
<LanguageForm
|
|
69
|
+
language={language}
|
|
70
|
+
formAction={updateLanguageWithId}
|
|
71
|
+
actionButtonText="Update Language"
|
|
72
|
+
isEditing={true}
|
|
73
|
+
allLanguages={allLanguages}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// app/cms/settings/languages/actions.ts
|
|
2
|
+
"use server";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
+
import { revalidatePath } from "next/cache";
|
|
6
|
+
import { redirect } from "next/navigation";
|
|
7
|
+
import type { Database } from "@nextblock-cms/db";
|
|
8
|
+
|
|
9
|
+
type Language = Database["public"]["Tables"]["languages"]["Row"];
|
|
10
|
+
|
|
11
|
+
// Helper to check admin role
|
|
12
|
+
async function verifyAdmin(supabase: ReturnType<typeof createClient>): Promise<boolean> {
|
|
13
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
14
|
+
if (!user) return false;
|
|
15
|
+
const { data: profile } = await supabase
|
|
16
|
+
.from("profiles")
|
|
17
|
+
.select("role")
|
|
18
|
+
.eq("id", user.id)
|
|
19
|
+
.single();
|
|
20
|
+
return profile?.role === "ADMIN";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getLanguages(): Promise<{ data: Language[] | null; error: string | null; }> {
|
|
24
|
+
const supabase = createClient();
|
|
25
|
+
const { data, error } = await supabase
|
|
26
|
+
.from("languages")
|
|
27
|
+
.select("id, code, name, is_default, is_active, created_at, updated_at")
|
|
28
|
+
.order("is_default", { ascending: false }) // Default first
|
|
29
|
+
.order("name", { ascending: true });
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
console.error("Error fetching languages:", error);
|
|
33
|
+
return { data: null, error: `Failed to fetch languages: ${error.message}` };
|
|
34
|
+
}
|
|
35
|
+
return { data, error: null };
|
|
36
|
+
}
|
|
37
|
+
export async function getActiveLanguagesServerSide(): Promise<Language[]> {
|
|
38
|
+
const supabase = createClient();
|
|
39
|
+
const { data, error } = await supabase
|
|
40
|
+
.from("languages")
|
|
41
|
+
.select("id, code, name, is_default, is_active, created_at, updated_at")
|
|
42
|
+
.eq("is_active", true) // Assuming there's an is_active column
|
|
43
|
+
.order("name", { ascending: true });
|
|
44
|
+
|
|
45
|
+
if (error) {
|
|
46
|
+
console.error("Error fetching active languages:", error);
|
|
47
|
+
throw new Error("Failed to fetch active languages.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data || [];
|
|
51
|
+
}
|
|
52
|
+
export async function getLanguageByCode(code: string): Promise<{ data: Language | null; error: string | null; }> {
|
|
53
|
+
const supabase = createClient();
|
|
54
|
+
const { data, error } = await supabase
|
|
55
|
+
.from("languages")
|
|
56
|
+
.select("id, code, name, is_default, is_active, created_at, updated_at")
|
|
57
|
+
.eq("code", code)
|
|
58
|
+
.single();
|
|
59
|
+
|
|
60
|
+
if (error) {
|
|
61
|
+
console.error(`Error fetching language by code ${code}:`, error);
|
|
62
|
+
return { data: null, error: `Failed to fetch language by code ${code}: ${error.message}` };
|
|
63
|
+
}
|
|
64
|
+
return { data, error: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type UpsertLanguagePayload = {
|
|
68
|
+
code: string;
|
|
69
|
+
name: string;
|
|
70
|
+
is_default: boolean;
|
|
71
|
+
is_active: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export async function createLanguage(formData: FormData) {
|
|
75
|
+
const supabase = createClient();
|
|
76
|
+
|
|
77
|
+
if (!(await verifyAdmin(supabase))) {
|
|
78
|
+
return { error: "Unauthorized: Admin role required." };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rawFormData = {
|
|
82
|
+
code: formData.get("code") as string,
|
|
83
|
+
name: formData.get("name") as string,
|
|
84
|
+
is_default: formData.get("is_default") === "on", // Checkbox value
|
|
85
|
+
is_active: formData.get("is_active") === "on",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (!rawFormData.code || !rawFormData.name) {
|
|
89
|
+
return { error: "Missing required fields: code or name." };
|
|
90
|
+
}
|
|
91
|
+
if (rawFormData.code.length > 10) { // Basic validation
|
|
92
|
+
return { error: "Language code is too long (max 10 characters)." };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
const languageData: UpsertLanguagePayload = {
|
|
97
|
+
...rawFormData,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// If setting this language as default, unset any other default language first
|
|
101
|
+
if (languageData.is_default) {
|
|
102
|
+
const { error: unsetError } = await supabase
|
|
103
|
+
.from("languages")
|
|
104
|
+
.update({ is_default: false })
|
|
105
|
+
.eq("is_default", true);
|
|
106
|
+
if (unsetError) {
|
|
107
|
+
console.error("Error unsetting previous default language:", unsetError);
|
|
108
|
+
return { error: `Failed to unset previous default language: ${unsetError.message}` };
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// Ensure there's at least one default language if unsetting the current one
|
|
112
|
+
// This logic is complex if done here. The DB unique index on (is_default) WHERE is_default=true handles one default.
|
|
113
|
+
// If unchecking the *only* default, the DB might prevent it or it might allow no default.
|
|
114
|
+
// It's better to handle "setting a new default" as the primary way to change the default.
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
const { data, error } = await supabase
|
|
119
|
+
.from("languages")
|
|
120
|
+
.insert(languageData)
|
|
121
|
+
.select()
|
|
122
|
+
.single();
|
|
123
|
+
|
|
124
|
+
if (error) {
|
|
125
|
+
console.error("Error creating language:", error);
|
|
126
|
+
if (error.code === '23505') { // Unique violation
|
|
127
|
+
if (error.message.includes('languages_code_key')) {
|
|
128
|
+
return { error: `Language code '${languageData.code}' already exists.` };
|
|
129
|
+
}
|
|
130
|
+
if (error.message.includes('ensure_single_default_language_idx')) {
|
|
131
|
+
return { error: `Cannot set this language as default. Another language is already default, or an error occurred unsetting it.` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { error: `Failed to create language: ${error.message}` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
revalidatePath("/cms/settings/languages");
|
|
138
|
+
revalidatePath("/"); // Revalidate home page as language switcher might change
|
|
139
|
+
if (data?.id) {
|
|
140
|
+
redirect(`/cms/settings/languages/${data.id}/edit?success=Language created successfully`);
|
|
141
|
+
} else {
|
|
142
|
+
redirect(`/cms/settings/languages?success=Language created successfully`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function updateLanguage(languageId: number, formData: FormData) {
|
|
147
|
+
const supabase = createClient();
|
|
148
|
+
|
|
149
|
+
if (!(await verifyAdmin(supabase))) {
|
|
150
|
+
return { error: "Unauthorized: Admin role required." };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rawFormData = {
|
|
154
|
+
code: formData.get("code") as string,
|
|
155
|
+
name: formData.get("name") as string,
|
|
156
|
+
is_default: formData.get("is_default") === "on",
|
|
157
|
+
is_active: formData.get("is_active") === "on",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (!rawFormData.code || !rawFormData.name) {
|
|
161
|
+
return { error: "Missing required fields: code or name." };
|
|
162
|
+
}
|
|
163
|
+
if (rawFormData.code.length > 10) {
|
|
164
|
+
return { error: "Language code is too long (max 10 characters)." };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const languageData: Partial<UpsertLanguagePayload> = {
|
|
168
|
+
...rawFormData,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// If setting this language as default, unset any other default language first
|
|
172
|
+
if (languageData.is_default) {
|
|
173
|
+
const { error: unsetError } = await supabase
|
|
174
|
+
.from("languages")
|
|
175
|
+
.update({ is_default: false })
|
|
176
|
+
.eq("is_default", true)
|
|
177
|
+
.neq("id", languageId); // Don't unset self if it was already default
|
|
178
|
+
if (unsetError) {
|
|
179
|
+
console.error("Error unsetting previous default language:", unsetError);
|
|
180
|
+
return { error: `Failed to unset previous default language: ${unsetError.message}` };
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Check if we are trying to uncheck the *only* default language.
|
|
184
|
+
// The DB unique index `ensure_single_default_language_idx` might prevent this if it leads to zero defaults.
|
|
185
|
+
// It's safer to enforce that one language must always be default through UI logic (e.g., disable unchecking if it's the only default).
|
|
186
|
+
const { data: currentLang } = await supabase.from("languages").select("is_default").eq("id", languageId).single();
|
|
187
|
+
if (currentLang?.is_default && !languageData.is_default) {
|
|
188
|
+
const { count } = await supabase.from("languages").select('*', { count: 'exact', head: true }).eq("is_default", true);
|
|
189
|
+
if (count === 1) {
|
|
190
|
+
return { error: "Cannot unset the only default language. Please set another language as default first." };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { error } = await supabase
|
|
196
|
+
.from("languages")
|
|
197
|
+
.update(languageData)
|
|
198
|
+
.eq("id", languageId);
|
|
199
|
+
|
|
200
|
+
if (error) {
|
|
201
|
+
console.error("Error updating language:", error);
|
|
202
|
+
if (error.code === '23505') {
|
|
203
|
+
if (error.message.includes('languages_code_key')) {
|
|
204
|
+
return { error: `Language code '${languageData.code}' already exists for another language.` };
|
|
205
|
+
}
|
|
206
|
+
if (error.message.includes('ensure_single_default_language_idx')) {
|
|
207
|
+
return { error: `Database constraint: Only one language can be default.` };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { error: `Failed to update language: ${error.message}` };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
revalidatePath("/cms/settings/languages");
|
|
214
|
+
revalidatePath("/");
|
|
215
|
+
redirect(`/cms/settings/languages/${languageId}/edit?success=Language updated successfully`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function deleteLanguage(languageId: number) {
|
|
219
|
+
const supabase = createClient();
|
|
220
|
+
|
|
221
|
+
if (!(await verifyAdmin(supabase))) {
|
|
222
|
+
return { error: "Unauthorized: Admin role required." };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Critical check: Prevent deleting the default language if it's the only one or default.
|
|
226
|
+
const { data: langToDelete, error: langFetchError } = await supabase
|
|
227
|
+
.from("languages")
|
|
228
|
+
.select("is_default, code")
|
|
229
|
+
.eq("id", languageId)
|
|
230
|
+
.single();
|
|
231
|
+
|
|
232
|
+
if (langFetchError || !langToDelete) {
|
|
233
|
+
return { error: "Language not found or error fetching details." };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (langToDelete.is_default) {
|
|
237
|
+
const { count } = await supabase.from("languages").select('*', { count: 'exact', head: true });
|
|
238
|
+
if (count === 1) {
|
|
239
|
+
return { error: "Cannot delete the only language, especially if it's default." };
|
|
240
|
+
}
|
|
241
|
+
return { error: "Cannot delete the default language. Set another language as default first." };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// WARNING: Deleting a language will cascade delete all associated content
|
|
245
|
+
// (pages, posts, blocks, navigation_items) due to foreign key constraints with ON DELETE CASCADE.
|
|
246
|
+
// This is a very destructive operation. A confirmation step in the UI is crucial.
|
|
247
|
+
|
|
248
|
+
const { error } = await supabase
|
|
249
|
+
.from("languages")
|
|
250
|
+
.delete()
|
|
251
|
+
.eq("id", languageId);
|
|
252
|
+
|
|
253
|
+
if (error) {
|
|
254
|
+
console.error("Error deleting language:", error);
|
|
255
|
+
return { error: `Failed to delete language: ${error.message}. Check if content is still linked.` };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
revalidatePath("/cms/settings/languages");
|
|
259
|
+
revalidatePath("/");
|
|
260
|
+
redirect("/cms/settings/languages?success=Language deleted successfully. All associated content has also been removed.");
|
|
261
|
+
}
|