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,76 @@
1
+ "use client";
2
+
3
+ import { DropdownMenuItem } from "@nextblock-cms/ui";
4
+ import { Trash2, ShieldAlert } from "lucide-react";
5
+ import { deleteLanguage } from "../actions"; // Server action
6
+ import type { Database } from "@nextblock-cms/db";
7
+ import { useTransition, useState } from 'react';
8
+ import { ConfirmationModal } from "@/app/cms/components/ConfirmationModal";
9
+
10
+ type Language = Database['public']['Tables']['languages']['Row'];
11
+
12
+ interface DeleteLanguageClientButtonProps {
13
+ language: Language;
14
+ }
15
+
16
+ export default function DeleteLanguageClientButton({ language }: DeleteLanguageClientButtonProps) {
17
+ const [isPending, startTransition] = useTransition();
18
+ const [isModalOpen, setIsModalOpen] = useState(false);
19
+
20
+ // A more robust check for "is_default and is the only default language" should ideally
21
+ // be handled by the server action or by passing allLanguages to this component.
22
+ // For UI hint, this is a simplified check. The server action has the final say.
23
+ const isDefaultLanguage = language.is_default;
24
+
25
+ const handleDeleteConfirm = () => {
26
+ // The server-side `deleteLanguage` action already checks if it's the default
27
+ // and if it's the only language. This client-side check is for immediate UX.
28
+ if (isDefaultLanguage) {
29
+ // The server action has a more robust check for "only default"
30
+ // For now, a simple alert for any default language.
31
+ alert("Cannot delete the default language. Please set another language as default first, or ensure this is not the only language.");
32
+ setIsModalOpen(false);
33
+ return;
34
+ }
35
+
36
+ startTransition(async () => {
37
+ const result = await deleteLanguage(language.id); // Call the server action
38
+ if (result?.error) {
39
+ alert(`Error: ${result.error}`);
40
+ // In a real app, use a toast or a more integrated notification system
41
+ }
42
+ // Revalidation and redirection are handled by the server action itself.
43
+ setIsModalOpen(false);
44
+ });
45
+ };
46
+
47
+ return (
48
+ <>
49
+ <DropdownMenuItem
50
+ className={`hover:!bg-red-50 dark:hover:!bg-red-700/20 ${
51
+ isDefaultLanguage // Visually hint if it's default, server action has final say on deletability
52
+ ? "text-muted-foreground cursor-not-allowed hover:!text-muted-foreground"
53
+ : "text-red-600 hover:!text-red-600 cursor-pointer"
54
+ }`}
55
+ onSelect={(e) => e.preventDefault()} // Prevent menu closing immediately
56
+ onClick={() => !isPending && !isDefaultLanguage && setIsModalOpen(true)}
57
+ disabled={isPending || isDefaultLanguage} // Disable if pending or if it's the default language
58
+ >
59
+ <Trash2 className="mr-2 h-4 w-4" />
60
+ {isPending ? "Deleting..." : "Delete"}
61
+ {isDefaultLanguage && (
62
+ <span title="This is the default language. Deletion might be restricted.">
63
+ <ShieldAlert className="ml-auto h-4 w-4 text-amber-500" />
64
+ </span>
65
+ )}
66
+ </DropdownMenuItem>
67
+ <ConfirmationModal
68
+ isOpen={isModalOpen}
69
+ onClose={() => setIsModalOpen(false)}
70
+ onConfirm={handleDeleteConfirm}
71
+ title="Are you sure?"
72
+ description="This will permanently delete the language. This action cannot be undone."
73
+ />
74
+ </>
75
+ );
76
+ }
@@ -0,0 +1,167 @@
1
+ // app/cms/settings/languages/components/LanguageForm.tsx
2
+ "use client";
3
+
4
+ import React, { useState, useTransition, useEffect } from 'react';
5
+ import { useRouter, useSearchParams } from 'next/navigation';
6
+ import { Button } from '@nextblock-cms/ui';
7
+ import { Input } from '@nextblock-cms/ui';
8
+ import { Label } from '@nextblock-cms/ui';
9
+ import { Checkbox } from '@nextblock-cms/ui'; // Assuming shadcn/ui Checkbox
10
+ import type { Database } from "@nextblock-cms/db";
11
+
12
+ type Language = Database["public"]["Tables"]["languages"]["Row"];
13
+ import { useAuth } from '@/context/AuthContext';
14
+
15
+ interface LanguageFormProps {
16
+ language?: Language | null;
17
+ formAction: (formData: FormData) => Promise<{ error?: string } | void>;
18
+ actionButtonText?: string;
19
+ isEditing?: boolean;
20
+ allLanguages?: Language[]; // Pass all languages to check for "only default" scenario
21
+ }
22
+
23
+ export default function LanguageForm({
24
+ language,
25
+ formAction,
26
+ actionButtonText = "Save Language",
27
+ isEditing = false,
28
+ allLanguages = []
29
+ }: LanguageFormProps) {
30
+ const router = useRouter();
31
+ const searchParams = useSearchParams();
32
+ const [isPending, startTransition] = useTransition();
33
+ const { isAdmin, isLoading: authLoading } = useAuth();
34
+
35
+ const [code, setCode] = useState(language?.code || "");
36
+ const [name, setName] = useState(language?.name || "");
37
+ const [isDefault, setIsDefault] = useState(language?.is_default || false);
38
+ const [isActive, setIsActive] = useState(language?.is_active ?? true);
39
+
40
+ const [formMessage, setFormMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
41
+
42
+ useEffect(() => {
43
+ const successMessage = searchParams.get('success');
44
+ const errorMessage = searchParams.get('error');
45
+ if (successMessage) setFormMessage({ type: 'success', text: successMessage });
46
+ else if (errorMessage) setFormMessage({ type: 'error', text: errorMessage });
47
+ }, [searchParams]);
48
+
49
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
50
+ event.preventDefault();
51
+ setFormMessage(null);
52
+ const formData = new FormData(event.currentTarget);
53
+ // Checkbox value needs to be explicitly set if not checked
54
+ if (!isDefault) {
55
+ formData.delete('is_default'); // Remove if not checked, action handles "on" or missing
56
+ } else {
57
+ formData.set('is_default', 'on');
58
+ }
59
+ if (isActive) {
60
+ formData.set('is_active', 'on');
61
+ } else {
62
+ formData.delete('is_active');
63
+ }
64
+
65
+
66
+ startTransition(async () => {
67
+ const result = await formAction(formData);
68
+ if (result?.error) {
69
+ setFormMessage({ type: 'error', text: result.error });
70
+ }
71
+ // Success is handled by redirect with query param in server action
72
+ });
73
+ };
74
+
75
+ if (authLoading) return <div>Loading...</div>;
76
+ if (!isAdmin) return <div>Access Denied. Admin role required.</div>;
77
+
78
+ const isTheOnlyDefaultLanguage = isEditing && language?.is_default && allLanguages.filter(l => l.is_default).length === 1;
79
+
80
+
81
+ return (
82
+ <form onSubmit={handleSubmit} className="space-y-6">
83
+ {formMessage && (
84
+ <div
85
+ className={`p-3 rounded-md text-sm ${
86
+ formMessage.type === 'success'
87
+ ? 'bg-green-100 text-green-700 border border-green-200'
88
+ : 'bg-red-100 text-red-700 border border-red-200'
89
+ }`}
90
+ >
91
+ {formMessage.text}
92
+ </div>
93
+ )}
94
+ <div>
95
+ <Label htmlFor="code">Language Code (e.g., en, fr-CA)</Label>
96
+ <Input
97
+ id="code"
98
+ name="code"
99
+ value={code}
100
+ onChange={(e) => setCode(e.target.value)}
101
+ required
102
+ maxLength={10}
103
+ className="mt-1"
104
+ placeholder="en"
105
+ />
106
+ <p className="text-xs text-muted-foreground mt-1">Short, unique BCP 47 language tag.</p>
107
+ </div>
108
+
109
+ <div>
110
+ <Label htmlFor="name">Display Name</Label>
111
+ <Input
112
+ id="name"
113
+ name="name"
114
+ value={name}
115
+ onChange={(e) => setName(e.target.value)}
116
+ required
117
+ className="mt-1"
118
+ placeholder="English"
119
+ />
120
+ </div>
121
+
122
+ <div className="flex items-center space-x-2 pt-2">
123
+ <Checkbox
124
+ id="is_default"
125
+ name="is_default"
126
+ checked={isDefault}
127
+ onCheckedChange={(checked) => setIsDefault(checked as boolean)}
128
+ disabled={isTheOnlyDefaultLanguage && isDefault} // Prevent unchecking the only default
129
+ />
130
+ <Label htmlFor="is_default" className="font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
131
+ Set as Default Language
132
+ </Label>
133
+ </div>
134
+ {isTheOnlyDefaultLanguage && isDefault && (
135
+ <p className="text-xs text-amber-600">This is the only default language. To change, set another language as default.</p>
136
+ )}
137
+
138
+ <div className="flex items-center space-x-2 pt-2">
139
+ <Checkbox
140
+ id="is_active"
141
+ name="is_active"
142
+ checked={isActive}
143
+ onCheckedChange={(checked) => setIsActive(checked as boolean)}
144
+ />
145
+ <Label htmlFor="is_active" className="font-normal leading-none">
146
+ Language is Active
147
+ </Label>
148
+ </div>
149
+ <p className="text-xs text-muted-foreground -mt-1">Inactive languages are hidden from public view but still available for content management.</p>
150
+
151
+
152
+ <div className="flex justify-end space-x-3 pt-4">
153
+ <Button
154
+ type="button"
155
+ variant="outline"
156
+ onClick={() => router.push("/cms/settings/languages")}
157
+ disabled={isPending}
158
+ >
159
+ Cancel
160
+ </Button>
161
+ <Button type="submit" disabled={isPending || authLoading}>
162
+ {isPending ? "Saving..." : actionButtonText}
163
+ </Button>
164
+ </div>
165
+ </form>
166
+ );
167
+ }
@@ -0,0 +1,34 @@
1
+ // app/cms/settings/languages/new/page.tsx
2
+ import React from "react";
3
+ import LanguageForm from "../components/LanguageForm";
4
+ import { createLanguage } from "../actions";
5
+ import { createClient } from "@nextblock-cms/db/server"; // To fetch all languages for the form logic
6
+ import type { Database } from "@nextblock-cms/db";
7
+
8
+ type Language = Database['public']['Tables']['languages']['Row'];
9
+
10
+ async function getAllLanguages(): Promise<Language[]> {
11
+ const supabase = createClient();
12
+ const { data, error } = await supabase.from("languages").select("*");
13
+ if (error) {
14
+ console.error("Error fetching languages for new page form:", error);
15
+ return [];
16
+ }
17
+ return data || [];
18
+ }
19
+
20
+
21
+ export default async function NewLanguagePage() {
22
+ const allLanguages = await getAllLanguages();
23
+ return (
24
+ <div className="max-w-xl mx-auto">
25
+ <h1 className="text-2xl font-bold mb-6">Add New Language</h1>
26
+ <LanguageForm
27
+ formAction={createLanguage}
28
+ actionButtonText="Add Language"
29
+ isEditing={false}
30
+ allLanguages={allLanguages}
31
+ />
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,156 @@
1
+ // app/cms/settings/languages/page.tsx
2
+ import React from 'react';
3
+ import { createClient } from "@nextblock-cms/db/server";
4
+ import Link from "next/link";
5
+ import { Button } from "@nextblock-cms/ui";
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "@nextblock-cms/ui";
14
+ import { Badge } from "@nextblock-cms/ui";
15
+ import { MoreHorizontal, PlusCircle, Edit3, Languages as LanguagesIcon, ShieldAlert } from "lucide-react";
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuTrigger,
21
+ DropdownMenuSeparator,
22
+ } from "@nextblock-cms/ui";
23
+ import type { Database } from "@nextblock-cms/db";
24
+ import DeleteLanguageClientButton from './components/DeleteLanguageButton';
25
+
26
+ type Language = Database['public']['Tables']['languages']['Row'];
27
+
28
+ async function getLanguages(): Promise<Language[]> {
29
+ const supabase = createClient();
30
+ const { data, error } = await supabase
31
+ .from("languages")
32
+ .select("*")
33
+ .order("name", { ascending: true });
34
+
35
+ if (error) {
36
+ console.error("Error fetching languages:", error);
37
+ return [];
38
+ }
39
+ return data || [];
40
+ }
41
+
42
+ export default async function CmsLanguagesListPage() {
43
+ const languages = await getLanguages();
44
+ // The following line for searchParams will cause an error during static generation or if window is not defined.
45
+ // It's better to pass searchParams as props if needed from the page component.
46
+ // For this specific page, success messages are handled by redirect query params which Next.js makes available in page props.
47
+ // Let's assume `props.searchParams.success` would be used if passed.
48
+ // const searchParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
49
+ // const successMessage = searchParams.get('success');
50
+ const successMessage: string | null = null;
51
+ // If you need to read searchParams, ensure your page component accepts them:
52
+ // export default async function CmsLanguagesListPage({ searchParams }: { searchParams?: { success?: string } }) {
53
+ // successMessage = searchParams?.success ?? null;
54
+ // }
55
+
56
+
57
+ return (
58
+ <div className="w-full">
59
+ <div className="flex justify-between items-center mb-6">
60
+ <h1 className="text-2xl font-semibold">Manage Languages</h1>
61
+ <Button variant="default" asChild>
62
+ <Link href="/cms/settings/languages/new">
63
+ <PlusCircle className="mr-2 h-4 w-4" /> Add New Language
64
+ </Link>
65
+ </Button>
66
+ </div>
67
+
68
+ {successMessage && (
69
+ <div className="mb-4 p-3 rounded-md text-sm bg-green-100 text-green-700 border border-green-200">
70
+ {decodeURIComponent(successMessage)}
71
+ </div>
72
+ )}
73
+
74
+ {languages.length === 0 ? (
75
+ <div className="text-center py-10 border rounded-lg">
76
+ <LanguagesIcon className="mx-auto h-12 w-12 text-muted-foreground" />
77
+ <h3 className="mt-2 text-sm font-medium text-foreground">No languages configured</h3>
78
+ <p className="mt-1 text-sm text-muted-foreground">
79
+ Add languages to support multilingual content.
80
+ </p>
81
+ <div className="mt-6">
82
+ <Button asChild>
83
+ <Link href="/cms/settings/languages/new">
84
+ <PlusCircle className="mr-2 h-4 w-4" /> Add Language
85
+ </Link>
86
+ </Button>
87
+ </div>
88
+ </div>
89
+ ) : (
90
+ <div className="rounded-lg border overflow-hidden">
91
+ <Table>
92
+ <TableHeader>
93
+ <TableRow>
94
+ <TableHead className="w-[200px]">Name</TableHead>
95
+ <TableHead>Code</TableHead>
96
+ <TableHead>Default</TableHead>
97
+ <TableHead>Created At</TableHead>
98
+ <TableHead className="text-right w-[80px]">Actions</TableHead>
99
+ </TableRow>
100
+ </TableHeader>
101
+ <TableBody>
102
+ {languages.map((lang) => (
103
+ <TableRow key={lang.id}>
104
+ <TableCell className="font-medium">{lang.name}</TableCell>
105
+ <TableCell>
106
+ <Badge variant="secondary">{lang.code}</Badge>
107
+ </TableCell>
108
+ <TableCell>
109
+ {lang.is_default ? (
110
+ <Badge className="bg-blue-500 hover:bg-blue-600 text-white">Default</Badge>
111
+ ) : (
112
+ <span className="text-xs text-muted-foreground">-</span>
113
+ )}
114
+ </TableCell>
115
+ <TableCell className="text-xs text-muted-foreground">
116
+ {lang.created_at ? new Date(lang.created_at).toLocaleDateString() : '-'}
117
+ </TableCell>
118
+ <TableCell className="text-right">
119
+ <DropdownMenu>
120
+ <DropdownMenuTrigger asChild>
121
+ <Button variant="ghost" size="icon">
122
+ <MoreHorizontal className="h-4 w-4" />
123
+ <span className="sr-only">Language actions</span>
124
+ </Button>
125
+ </DropdownMenuTrigger>
126
+ <DropdownMenuContent align="end">
127
+ <DropdownMenuItem asChild>
128
+ <Link href={`/cms/settings/languages/${lang.id}/edit`} className="flex items-center cursor-pointer">
129
+ <Edit3 className="mr-2 h-4 w-4" /> Edit
130
+ </Link>
131
+ </DropdownMenuItem>
132
+ <DropdownMenuSeparator />
133
+ <DeleteLanguageClientButton language={lang} />
134
+ </DropdownMenuContent>
135
+ </DropdownMenu>
136
+ </TableCell>
137
+ </TableRow>
138
+ ))}
139
+ </TableBody>
140
+ </Table>
141
+ </div>
142
+ )}
143
+ <div className="mt-6 p-4 border border-amber-300 bg-amber-50 dark:bg-amber-900/30 rounded-lg">
144
+ <div className="flex items-start">
145
+ <ShieldAlert className="h-5 w-5 text-amber-600 dark:text-amber-400 mr-3 flex-shrink-0 mt-0.5" />
146
+ <div>
147
+ <h4 className="text-sm font-semibold text-amber-700 dark:text-amber-300">Important Note on Deleting Languages</h4>
148
+ <p className="text-xs text-amber-600 dark:text-amber-400 mt-1">
149
+ Deleting a language is a destructive action. All content (pages, posts, blocks, navigation items) specifically associated with that language will be permanently deleted due to database cascade rules. Please ensure this is intended before proceeding. You cannot delete the current default language if it is the only one.
150
+ </p>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
@@ -0,0 +1,19 @@
1
+ import LogoForm from '../../components/LogoForm'
2
+ import { updateLogo, getLogoById } from '../../actions'
3
+ import { notFound } from 'next/navigation'
4
+
5
+ export default async function EditLogoPage(props: { params: Promise<{ id: string }> }) {
6
+ const params = await props.params
7
+ const logo = await getLogoById(params.id)
8
+
9
+ if (!logo) {
10
+ notFound()
11
+ }
12
+
13
+ return (
14
+ <div>
15
+ <h1 className="text-2xl font-semibold mb-6">Edit Logo</h1>
16
+ <LogoForm logo={logo} action={updateLogo as any} />
17
+ </div>
18
+ )
19
+ }
@@ -0,0 +1,114 @@
1
+ 'use server'
2
+
3
+ import { createClient } from '@nextblock-cms/db/server'
4
+ import { revalidatePath } from 'next/cache'
5
+ import { redirect } from 'next/navigation'
6
+ import type { Logo } from './types'
7
+
8
+ export async function createLogo(payload: {
9
+ name: string
10
+ media_id: string
11
+ }): Promise<{ success: boolean; error?: string }> {
12
+ const supabase = createClient()
13
+
14
+ const { error } = await supabase.from('logos').insert(payload)
15
+
16
+ if (error) {
17
+ console.error('Error creating logo:', error)
18
+ return { success: false, error: error.message }
19
+ }
20
+
21
+ revalidatePath('/cms/settings/logos')
22
+ return { success: true }
23
+ }
24
+
25
+ export async function updateLogo(payload: {
26
+ id: string
27
+ name: string
28
+ media_id: string
29
+ }) {
30
+ const supabase = createClient()
31
+ const { id, ...data } = payload
32
+
33
+ const { error } = await supabase.from('logos').update(data).eq('id', id)
34
+
35
+ if (error) {
36
+ console.error('Error updating logo:', error)
37
+ // Optionally, handle the error more gracefully
38
+ // redirect('/error?message=Could not update logo')
39
+ return
40
+ }
41
+
42
+ revalidatePath('/cms/settings/logos')
43
+ revalidatePath(`/cms/settings/logos/${id}/edit`)
44
+ redirect('/cms/settings/logos')
45
+ }
46
+
47
+ export async function deleteLogo(id: string) {
48
+ const supabase = createClient()
49
+
50
+ const { error } = await supabase.from('logos').delete().eq('id', id)
51
+
52
+ if (error) {
53
+ console.error('Error deleting logo:', error)
54
+ // Optionally, handle the error more gracefully
55
+ // redirect('/error?message=Could not delete logo')
56
+ return
57
+ }
58
+
59
+ revalidatePath('/cms/settings/logos')
60
+ }
61
+
62
+ export async function getLogos() {
63
+ const supabase = createClient()
64
+ const { data, error } = await supabase
65
+ .from('logos')
66
+ .select('*, media:media_id(*)')
67
+ .order('created_at', { ascending: false })
68
+
69
+ if (error) {
70
+ console.error('Error fetching logos:', error.message)
71
+ throw new Error(`Failed to fetch logos: ${error.message}`)
72
+ }
73
+
74
+ return data
75
+ }
76
+
77
+ export async function getLogoById(id: string) {
78
+ const supabase = createClient()
79
+ const { data, error } = await supabase
80
+ .from('logos')
81
+ .select('*, media:media_id(*)')
82
+ .eq('id', id)
83
+ .single()
84
+
85
+ if (error) {
86
+ console.error(`Error fetching logo by id ${id}:`, error.message)
87
+ return null
88
+ }
89
+
90
+ return data
91
+ }
92
+
93
+ export async function getActiveLogo(): Promise<Logo | null> {
94
+ const supabase = createClient()
95
+
96
+ const { data, error } = await supabase
97
+ .from('logos')
98
+ .select(
99
+ `
100
+ *,
101
+ media:media_id (*)
102
+ `
103
+ )
104
+ .order('created_at', { ascending: false })
105
+ .limit(1)
106
+ .maybeSingle()
107
+
108
+ if (error) {
109
+ console.error('Error fetching active logo:', error.message)
110
+ throw new Error(`Failed to fetch active logo: ${error.message}`)
111
+ }
112
+
113
+ return data as Logo | null
114
+ }