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.
Files changed (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. 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
+ }