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,358 @@
|
|
|
1
|
+
// app/cms/navigation/actions.ts
|
|
2
|
+
"use server";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
+
import { revalidatePath, unstable_noStore } from "next/cache";
|
|
6
|
+
import { redirect } from "next/navigation";
|
|
7
|
+
import type { Database } from "@nextblock-cms/db";
|
|
8
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
+
|
|
10
|
+
type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
|
|
11
|
+
type MenuLocation = Database['public']['Enums']['menu_location'];
|
|
12
|
+
|
|
13
|
+
// Helper to check admin role
|
|
14
|
+
async function isAdminUser(supabase: ReturnType<typeof createClient>): Promise<boolean> {
|
|
15
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
16
|
+
if (!user) return false;
|
|
17
|
+
const { data: profile } = await supabase
|
|
18
|
+
.from("profiles")
|
|
19
|
+
.select("role")
|
|
20
|
+
.eq("id", user.id)
|
|
21
|
+
.single();
|
|
22
|
+
return profile?.role === "ADMIN";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type UpsertNavigationItemPayload = {
|
|
26
|
+
language_id: number;
|
|
27
|
+
menu_key: MenuLocation;
|
|
28
|
+
label: string;
|
|
29
|
+
url: string;
|
|
30
|
+
parent_id?: number | null;
|
|
31
|
+
order?: number;
|
|
32
|
+
page_id?: number | null;
|
|
33
|
+
translation_group_id: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Helper to generate a placeholder label for new translations
|
|
37
|
+
function generatePlaceholderLabel(originalLabel: string, langCode: string): string {
|
|
38
|
+
return `[${langCode.toUpperCase()}] ${originalLabel}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
export async function createNavigationItem(formData: FormData) {
|
|
43
|
+
const supabase = createClient();
|
|
44
|
+
|
|
45
|
+
if (!(await isAdminUser(supabase))) {
|
|
46
|
+
return { error: "Unauthorized: Admin role required." };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fromGroupId = formData.get("from_translation_group_id") as string | null;
|
|
50
|
+
const targetLangIdForTranslation = formData.get("target_language_id_for_translation") as string | null;
|
|
51
|
+
const initialMenuKeyFromParam = formData.get("menu_key_from_param") as MenuLocation | null; // Capture if passed for new translation
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const rawFormData = {
|
|
55
|
+
label: formData.get("label") as string,
|
|
56
|
+
url: formData.get("url") as string,
|
|
57
|
+
language_id: parseInt(formData.get("language_id") as string, 10),
|
|
58
|
+
menu_key: (initialMenuKeyFromParam || formData.get("menu_key")) as MenuLocation,
|
|
59
|
+
order: parseInt(formData.get("order") as string, 10) || 0,
|
|
60
|
+
parent_id: formData.get("parent_id") && formData.get("parent_id") !== "___NONE___" ? parseInt(formData.get("parent_id") as string, 10) : null,
|
|
61
|
+
page_id: formData.get("page_id") && formData.get("page_id") !== "___NONE___" ? parseInt(formData.get("page_id") as string, 10) : null,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (!rawFormData.label || !rawFormData.url || isNaN(rawFormData.language_id) || !rawFormData.menu_key) {
|
|
65
|
+
return { error: "Missing required fields: label, URL, language, or menu key." };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const translationGroupId = fromGroupId || uuidv4();
|
|
69
|
+
|
|
70
|
+
const navData: UpsertNavigationItemPayload = {
|
|
71
|
+
...rawFormData,
|
|
72
|
+
translation_group_id: translationGroupId,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const { data: newNavItem, error } = await supabase
|
|
76
|
+
.from("navigation_items")
|
|
77
|
+
.insert(navData)
|
|
78
|
+
.select()
|
|
79
|
+
.single();
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
console.error("Error creating navigation item:", error);
|
|
83
|
+
return { error: `Failed to create item: ${error.message}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let successMessage = "Navigation item created successfully.";
|
|
87
|
+
|
|
88
|
+
if (newNavItem && !fromGroupId && !targetLangIdForTranslation) {
|
|
89
|
+
// Get other languages to create placeholder translations
|
|
90
|
+
const { data: languages, error: langError } = await supabase
|
|
91
|
+
.from("languages")
|
|
92
|
+
.select("id, code")
|
|
93
|
+
.neq("id", newNavItem.language_id);
|
|
94
|
+
|
|
95
|
+
if (langError) {
|
|
96
|
+
console.error("Error fetching other languages for nav item auto-creation:", langError);
|
|
97
|
+
} else if (languages && languages.length > 0) {
|
|
98
|
+
let parentTranslationGroupId: string | null = null;
|
|
99
|
+
if (newNavItem.parent_id) {
|
|
100
|
+
const { data: parentItem, error: parentError } = await supabase
|
|
101
|
+
.from("navigation_items")
|
|
102
|
+
.select("translation_group_id")
|
|
103
|
+
.eq("id", newNavItem.parent_id)
|
|
104
|
+
.single();
|
|
105
|
+
if (parentError) {
|
|
106
|
+
console.error(`Error fetching parent translation group ID:`, parentError);
|
|
107
|
+
} else {
|
|
108
|
+
parentTranslationGroupId = parentItem.translation_group_id;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let pageTranslationGroupId: string | null = null;
|
|
113
|
+
if (newNavItem.page_id) {
|
|
114
|
+
const { data: linkedPage, error: pageError } = await supabase
|
|
115
|
+
.from("pages")
|
|
116
|
+
.select("translation_group_id")
|
|
117
|
+
.eq("id", newNavItem.page_id)
|
|
118
|
+
.single();
|
|
119
|
+
if (pageError) {
|
|
120
|
+
console.error(`Error fetching page translation group ID:`, pageError);
|
|
121
|
+
} else if (linkedPage) {
|
|
122
|
+
pageTranslationGroupId = linkedPage.translation_group_id;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let placeholderCreations = 0;
|
|
127
|
+
for (const lang of languages) {
|
|
128
|
+
let translatedParentId: number | null = null;
|
|
129
|
+
if (parentTranslationGroupId) {
|
|
130
|
+
const { data: translatedParent } = await supabase
|
|
131
|
+
.from("navigation_items")
|
|
132
|
+
.select("id")
|
|
133
|
+
.eq("translation_group_id", parentTranslationGroupId)
|
|
134
|
+
.eq("language_id", lang.id)
|
|
135
|
+
.single();
|
|
136
|
+
if (translatedParent) {
|
|
137
|
+
translatedParentId = translatedParent.id;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let translatedPageId: number | null = null;
|
|
142
|
+
if (pageTranslationGroupId) {
|
|
143
|
+
const { data: translatedPage } = await supabase
|
|
144
|
+
.from("pages")
|
|
145
|
+
.select("id")
|
|
146
|
+
.eq("translation_group_id", pageTranslationGroupId)
|
|
147
|
+
.eq("language_id", lang.id)
|
|
148
|
+
.single();
|
|
149
|
+
if (translatedPage) {
|
|
150
|
+
translatedPageId = translatedPage.id;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const placeholderNavItemData: UpsertNavigationItemPayload = {
|
|
155
|
+
language_id: lang.id,
|
|
156
|
+
menu_key: newNavItem.menu_key,
|
|
157
|
+
label: generatePlaceholderLabel(newNavItem.label, lang.code),
|
|
158
|
+
url: newNavItem.url,
|
|
159
|
+
parent_id: translatedParentId,
|
|
160
|
+
order: newNavItem.order,
|
|
161
|
+
page_id: translatedPageId,
|
|
162
|
+
translation_group_id: newNavItem.translation_group_id,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const { error: placeholderError } = await supabase.from("navigation_items").insert(placeholderNavItemData);
|
|
166
|
+
if (placeholderError) {
|
|
167
|
+
console.error(`Error auto-creating nav item for language ${lang.code}:`, placeholderError);
|
|
168
|
+
} else {
|
|
169
|
+
placeholderCreations++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (placeholderCreations > 0) {
|
|
174
|
+
successMessage += ` ${placeholderCreations} translated version(s) also created (please edit their details).`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
revalidatePath("/cms/navigation");
|
|
181
|
+
if (newNavItem?.id) {
|
|
182
|
+
revalidatePath(`/cms/navigation/${newNavItem.id}/edit`);
|
|
183
|
+
redirect(`/cms/navigation/${newNavItem.id}/edit?success=${encodeURIComponent(successMessage)}`);
|
|
184
|
+
} else {
|
|
185
|
+
redirect(`/cms/navigation?success=${encodeURIComponent(successMessage)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function updateNavigationItem(itemId: number, formData: FormData) {
|
|
190
|
+
const supabase = createClient();
|
|
191
|
+
|
|
192
|
+
if (!(await isAdminUser(supabase))) {
|
|
193
|
+
return { error: "Unauthorized: Admin role required." };
|
|
194
|
+
}
|
|
195
|
+
const { data: existingItem, error: fetchError } = await supabase
|
|
196
|
+
.from("navigation_items")
|
|
197
|
+
.select("translation_group_id, language_id")
|
|
198
|
+
.eq("id", itemId)
|
|
199
|
+
.single();
|
|
200
|
+
|
|
201
|
+
if (fetchError || !existingItem) {
|
|
202
|
+
return { error: "Original navigation item not found or error fetching it." };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const rawFormData = {
|
|
206
|
+
label: formData.get("label") as string,
|
|
207
|
+
url: formData.get("url") as string,
|
|
208
|
+
language_id: parseInt(formData.get("language_id") as string, 10),
|
|
209
|
+
menu_key: formData.get("menu_key") as MenuLocation,
|
|
210
|
+
order: parseInt(formData.get("order") as string, 10) || 0,
|
|
211
|
+
parent_id: formData.get("parent_id") && formData.get("parent_id") !== "___NONE___" ? parseInt(formData.get("parent_id") as string, 10) : null,
|
|
212
|
+
page_id: formData.get("page_id") && formData.get("page_id") !== "___NONE___" ? parseInt(formData.get("page_id") as string, 10) : null,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (!rawFormData.label || !rawFormData.url || isNaN(rawFormData.language_id) || !rawFormData.menu_key) {
|
|
216
|
+
return { error: "Missing required fields: label, URL, language, or menu key." };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (rawFormData.language_id !== existingItem.language_id) {
|
|
220
|
+
return { error: "Changing the language of an existing navigation item version is not allowed. Create a new translation instead." };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const navData: Partial<Omit<UpsertNavigationItemPayload, 'translation_group_id'>> = {
|
|
224
|
+
...rawFormData,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const { error } = await supabase
|
|
228
|
+
.from("navigation_items")
|
|
229
|
+
.update(navData)
|
|
230
|
+
.eq("id", itemId);
|
|
231
|
+
|
|
232
|
+
if (error) {
|
|
233
|
+
console.error("Error updating navigation item:", error);
|
|
234
|
+
return { error: `Failed to update item: ${error.message}` };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
revalidatePath("/cms/navigation");
|
|
238
|
+
revalidatePath(`/cms/navigation/${itemId}/edit`);
|
|
239
|
+
redirect(`/cms/navigation/${itemId}/edit?success=Item updated successfully`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function deleteNavigationItem(itemId: number) {
|
|
243
|
+
const supabase = createClient();
|
|
244
|
+
|
|
245
|
+
if (!(await isAdminUser(supabase))) {
|
|
246
|
+
return { error: "Unauthorized: Admin role required." };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// First, get the translation_group_id for the item being deleted
|
|
250
|
+
const { data: itemToDelete, error: fetchError } = await supabase
|
|
251
|
+
.from("navigation_items")
|
|
252
|
+
.select("translation_group_id")
|
|
253
|
+
.eq("id", itemId)
|
|
254
|
+
.single();
|
|
255
|
+
|
|
256
|
+
if (fetchError || !itemToDelete) {
|
|
257
|
+
console.error("Error finding navigation item to delete:", fetchError);
|
|
258
|
+
return { error: "Failed to find the navigation item to delete." };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { translation_group_id } = itemToDelete;
|
|
262
|
+
|
|
263
|
+
if (!translation_group_id) {
|
|
264
|
+
console.error("Navigation item is missing a translation_group_id:", itemId);
|
|
265
|
+
return { error: "Cannot delete item as it is missing translation information." };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Now, delete all items in the same translation group
|
|
269
|
+
const { error: deleteError } = await supabase
|
|
270
|
+
.from("navigation_items")
|
|
271
|
+
.delete()
|
|
272
|
+
.eq("translation_group_id", translation_group_id);
|
|
273
|
+
|
|
274
|
+
if (deleteError) {
|
|
275
|
+
console.error("Error deleting navigation item and its translations:", deleteError);
|
|
276
|
+
return { error: `Failed to delete item and its translations: ${deleteError.message}` };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
revalidatePath("/cms/navigation");
|
|
280
|
+
|
|
281
|
+
return { success: true };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
export async function updateNavigationStructureBatch(
|
|
286
|
+
itemsToUpdate: Array<{ id: number; order: number; parent_id: number | null; }>
|
|
287
|
+
) {
|
|
288
|
+
const supabase = createClient();
|
|
289
|
+
if (!(await isAdminUser(supabase))) {
|
|
290
|
+
return { error: "Unauthorized: Admin role required for batch update." };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!itemsToUpdate || itemsToUpdate.length === 0) {
|
|
294
|
+
return { error: "No items provided for update." };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Supabase JS v2 doesn't have built-in transactions for multiple upserts like this directly.
|
|
298
|
+
// You'd typically loop and perform individual updates.
|
|
299
|
+
// If one fails, others might have succeeded. Consider rollback strategy if needed (more complex).
|
|
300
|
+
let CmsNavigationListPageFailedUpdates = 0;
|
|
301
|
+
for (const item of itemsToUpdate) {
|
|
302
|
+
const { error } = await supabase
|
|
303
|
+
.from("navigation_items")
|
|
304
|
+
.update({
|
|
305
|
+
order: item.order,
|
|
306
|
+
parent_id: item.parent_id,
|
|
307
|
+
updated_at: new Date().toISOString(), // Also update updated_at
|
|
308
|
+
})
|
|
309
|
+
.eq("id", item.id);
|
|
310
|
+
|
|
311
|
+
if (error) {
|
|
312
|
+
console.error(`Error updating nav item ${item.id}:`, error.message);
|
|
313
|
+
CmsNavigationListPageFailedUpdates++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (CmsNavigationListPageFailedUpdates > 0) {
|
|
318
|
+
return { error: `Failed to update ${CmsNavigationListPageFailedUpdates} item(s). Some changes might not have been saved.` };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
revalidatePath("/cms/navigation");
|
|
322
|
+
// No redirect needed here, as this is likely called via client-side transition
|
|
323
|
+
return { success: true, message: "Navigation structure updated." };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
// Fetches navigation items for a specific menu and language (used by public site Header/Footer)
|
|
328
|
+
export async function getNavigationMenu(menuKey: MenuLocation, languageCode: string): Promise<NavigationItem[]> {
|
|
329
|
+
const supabase = createClient(); // server client
|
|
330
|
+
unstable_noStore(); // Opt out of caching for this function
|
|
331
|
+
|
|
332
|
+
const { data: language, error: langError } = await supabase
|
|
333
|
+
.from("languages")
|
|
334
|
+
.select("id")
|
|
335
|
+
.eq("code", languageCode)
|
|
336
|
+
.single();
|
|
337
|
+
|
|
338
|
+
if (langError || !language) {
|
|
339
|
+
console.error(`Error fetching language ID for code ${languageCode} in getNavigationMenu:`, langError);
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const languageId = language.id;
|
|
344
|
+
|
|
345
|
+
const { data: items, error: itemsError } = await supabase
|
|
346
|
+
.from("navigation_items")
|
|
347
|
+
.select("*, pages(slug)") // Select all fields, including translation_group_id and linked page slug
|
|
348
|
+
.eq("menu_key", menuKey)
|
|
349
|
+
.eq("language_id", languageId)
|
|
350
|
+
.order("parent_id", { nullsFirst: true })
|
|
351
|
+
.order("order");
|
|
352
|
+
|
|
353
|
+
if (itemsError) {
|
|
354
|
+
console.error(`Error fetching navigation items for ${menuKey} (${languageCode}):`, itemsError);
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
return (items || []).map(item => ({...item, id: Number(item.id)}));
|
|
358
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { DropdownMenuItem } from "@nextblock-cms/ui";
|
|
5
|
+
import { Trash2 } from "lucide-react";
|
|
6
|
+
import { deleteNavigationItem } from "../actions";
|
|
7
|
+
import { ConfirmationModal } from "../../components/ConfirmationModal";
|
|
8
|
+
|
|
9
|
+
interface DeleteNavItemButtonProps {
|
|
10
|
+
itemId: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function DeleteNavItemButton({ itemId }: DeleteNavItemButtonProps) {
|
|
14
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
15
|
+
|
|
16
|
+
const onConfirm = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const result = await deleteNavigationItem(itemId);
|
|
19
|
+
if (result.success) {
|
|
20
|
+
window.location.reload();
|
|
21
|
+
} else {
|
|
22
|
+
console.error("Delete operation failed:", result.error);
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("Exception during delete action:", error);
|
|
26
|
+
} finally {
|
|
27
|
+
setIsModalOpen(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<DropdownMenuItem
|
|
34
|
+
className="text-red-600 hover:!text-red-600 hover:!bg-red-50 dark:hover:!bg-red-700/20"
|
|
35
|
+
onSelect={(e) => {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
setIsModalOpen(true);
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
41
|
+
Delete
|
|
42
|
+
</DropdownMenuItem>
|
|
43
|
+
<ConfirmationModal
|
|
44
|
+
isOpen={isModalOpen}
|
|
45
|
+
onClose={() => setIsModalOpen(false)}
|
|
46
|
+
onConfirm={onConfirm}
|
|
47
|
+
title="Are you sure?"
|
|
48
|
+
description="This will permanently delete the navigation item. This action cannot be undone."
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// app/cms/navigation/components/NavigationItemForm.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useEffect, useState, useTransition } 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 {
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from "@nextblock-cms/ui";
|
|
16
|
+
import type { Database } from "@nextblock-cms/db";
|
|
17
|
+
import { useAuth } from "@/context/AuthContext";
|
|
18
|
+
|
|
19
|
+
type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
|
|
20
|
+
type MenuLocation = Database['public']['Enums']['menu_location'];
|
|
21
|
+
type Language = Database['public']['Tables']['languages']['Row'];
|
|
22
|
+
type Page = Database['public']['Tables']['pages']['Row'];
|
|
23
|
+
|
|
24
|
+
interface NavigationItemFormProps {
|
|
25
|
+
item?: NavigationItem | null;
|
|
26
|
+
formAction: (formData: FormData) => Promise<{ error?: string } | void>;
|
|
27
|
+
actionButtonText?: string;
|
|
28
|
+
isEditing?: boolean;
|
|
29
|
+
languages: Language[];
|
|
30
|
+
parentItems: (Pick<NavigationItem, 'id' | 'label' | 'translation_group_id' | 'language_id' | 'parent_id'> & { menu_key: MenuLocation | null })[];
|
|
31
|
+
pages: Pick<Page, 'id' | 'title' | 'slug' | 'language_id'>[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function NavigationItemForm({
|
|
35
|
+
item,
|
|
36
|
+
formAction,
|
|
37
|
+
actionButtonText = "Save Item",
|
|
38
|
+
isEditing = false,
|
|
39
|
+
languages,
|
|
40
|
+
parentItems,
|
|
41
|
+
pages,
|
|
42
|
+
}: NavigationItemFormProps) {
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
const searchParams = useSearchParams();
|
|
45
|
+
const [isPending, startTransition] = useTransition();
|
|
46
|
+
const { isAdmin, isLoading: authLoading } = useAuth();
|
|
47
|
+
|
|
48
|
+
// For creating a new translation based on an existing item
|
|
49
|
+
const fromTranslationGroupId = searchParams.get("from_translation_group_id");
|
|
50
|
+
const targetLanguageIdForTranslation = searchParams.get("target_language_id_for_translation");
|
|
51
|
+
const initialMenuKeyFromParam = searchParams.get("menu_key") as MenuLocation | null;
|
|
52
|
+
const originalLabelFromParam = searchParams.get("original_label");
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const [label, setLabel] = useState(item?.label || (originalLabelFromParam ? `[Translate] ${originalLabelFromParam}` : ""));
|
|
56
|
+
const [url, setUrl] = useState(item?.url || (originalLabelFromParam ? "#" : "")); // Default to # if translating
|
|
57
|
+
const [languageId, setLanguageId] = useState<string>(
|
|
58
|
+
targetLanguageIdForTranslation || item?.language_id?.toString() || ""
|
|
59
|
+
);
|
|
60
|
+
const [menuKey, setMenuKey] = useState<MenuLocation | "">(
|
|
61
|
+
initialMenuKeyFromParam || item?.menu_key || "HEADER"
|
|
62
|
+
);
|
|
63
|
+
const [order, setOrder] = useState<string>(item?.order?.toString() || "0");
|
|
64
|
+
const [parentId, setParentId] = useState<string>(item?.parent_id?.toString() || "");
|
|
65
|
+
const [pageId, setPageId] = useState<string>(item?.page_id?.toString() || "");
|
|
66
|
+
|
|
67
|
+
const [availableLanguages] = useState<Language[]>(languages);
|
|
68
|
+
const [availablePages, setAvailablePages] = useState<Pick<Page, 'id' | 'title' | 'slug'>[]>([]);
|
|
69
|
+
const [availableParentItems, setAvailableParentItems] = useState<(Pick<NavigationItem, 'id' | 'label' | 'translation_group_id'> & { menu_key: MenuLocation | null })[]>([]);
|
|
70
|
+
const [dataLoading] = useState(false);
|
|
71
|
+
const [formMessage, setFormMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const successMessage = searchParams.get('success');
|
|
75
|
+
const errorMessage = searchParams.get('error');
|
|
76
|
+
if (successMessage) setFormMessage({ type: 'success', text: decodeURIComponent(successMessage) });
|
|
77
|
+
else if (errorMessage) setFormMessage({ type: 'error', text: decodeURIComponent(errorMessage) });
|
|
78
|
+
}, [searchParams]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isEditing && !languageId && !targetLanguageIdForTranslation && languages.length > 0) {
|
|
82
|
+
const defaultLang = languages.find(l => l.is_default) || languages[0];
|
|
83
|
+
if (defaultLang) setLanguageId(defaultLang.id.toString());
|
|
84
|
+
}
|
|
85
|
+
}, [isEditing, languageId, targetLanguageIdForTranslation, languages]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const currentLangId = languageId ? parseInt(languageId, 10) : null;
|
|
89
|
+
if (currentLangId) {
|
|
90
|
+
const filteredPages = pages.filter(p => p.language_id === currentLangId);
|
|
91
|
+
setAvailablePages(filteredPages);
|
|
92
|
+
|
|
93
|
+
const filteredParentItems = parentItems.filter(p => p.language_id === currentLangId && p.id !== item?.id);
|
|
94
|
+
setAvailableParentItems(filteredParentItems);
|
|
95
|
+
} else {
|
|
96
|
+
setAvailablePages([]);
|
|
97
|
+
setAvailableParentItems([]);
|
|
98
|
+
}
|
|
99
|
+
}, [languageId, pages, parentItems, item?.id]);
|
|
100
|
+
|
|
101
|
+
const handlePageSelect = (selectedPageId: string) => {
|
|
102
|
+
setPageId(selectedPageId);
|
|
103
|
+
const selectedPage = availablePages.find(p => p.id.toString() === selectedPageId);
|
|
104
|
+
if (selectedPage) {
|
|
105
|
+
setUrl(`/${selectedPage.slug}`);
|
|
106
|
+
} else if (selectedPageId === "___NONE___") {
|
|
107
|
+
setUrl("#"); // Reset URL if "None" is selected, or keep previous manual URL
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
setFormMessage(null);
|
|
114
|
+
const formData = new FormData(event.currentTarget);
|
|
115
|
+
|
|
116
|
+
// Append translation group ID if creating a new translation from an existing group
|
|
117
|
+
if (fromTranslationGroupId && !isEditing) {
|
|
118
|
+
formData.append("from_translation_group_id", fromTranslationGroupId);
|
|
119
|
+
}
|
|
120
|
+
if (targetLanguageIdForTranslation && !isEditing) {
|
|
121
|
+
formData.append("target_language_id_for_translation", targetLanguageIdForTranslation);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
startTransition(async () => {
|
|
126
|
+
const result = await formAction(formData);
|
|
127
|
+
if (result?.error) setFormMessage({ type: 'error', text: result.error });
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (authLoading || !isAdmin) {
|
|
132
|
+
return <div>Access Denied. Admin role required.</div>;
|
|
133
|
+
}
|
|
134
|
+
if (dataLoading && !item) return <div>Loading form data...</div>; // Show loading only if not editing an existing item with data
|
|
135
|
+
|
|
136
|
+
const menuLocations: MenuLocation[] = ['HEADER', 'FOOTER', 'SIDEBAR'];
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
140
|
+
{formMessage && (
|
|
141
|
+
<div className={`p-3 rounded-md text-sm ${formMessage.type === 'success' ? 'bg-green-100 text-green-700 border border-green-200' : 'bg-red-100 text-red-700 border border-red-200'}`}>
|
|
142
|
+
{formMessage.text}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Hidden input for from_translation_group_id if present in URL params and not editing */}
|
|
147
|
+
{!isEditing && fromTranslationGroupId && (
|
|
148
|
+
<input type="hidden" name="from_translation_group_id" value={fromTranslationGroupId} />
|
|
149
|
+
)}
|
|
150
|
+
{/* Hidden input for target_language_id_for_translation if present and not editing */}
|
|
151
|
+
{!isEditing && targetLanguageIdForTranslation && (
|
|
152
|
+
<input type="hidden" name="target_language_id_for_translation" value={targetLanguageIdForTranslation} />
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
<div>
|
|
157
|
+
<Label htmlFor="label">Label</Label>
|
|
158
|
+
<Input id="label" name="label" value={label} onChange={(e) => setLabel(e.target.value)} required className="mt-1" />
|
|
159
|
+
</div>
|
|
160
|
+
<div>
|
|
161
|
+
<Label htmlFor="page_id">Link to Internal Page (Optional)</Label>
|
|
162
|
+
<Select name="page_id" value={pageId} onValueChange={handlePageSelect} disabled={!languageId}>
|
|
163
|
+
<SelectTrigger className="mt-1"><SelectValue placeholder="None (Manual URL)" /></SelectTrigger>
|
|
164
|
+
<SelectContent>
|
|
165
|
+
<SelectItem value="___NONE___">None (Manual URL)</SelectItem>
|
|
166
|
+
{availablePages.map((p) => (
|
|
167
|
+
<SelectItem key={p.id} value={p.id.toString()}>{p.title} ({p.slug})</SelectItem>
|
|
168
|
+
))}
|
|
169
|
+
</SelectContent>
|
|
170
|
+
</Select>
|
|
171
|
+
<p className="text-xs text-muted-foreground mt-1">Selecting a page will auto-fill the URL (can be overridden). Requires language to be selected first.</p>
|
|
172
|
+
</div>
|
|
173
|
+
<div>
|
|
174
|
+
<Label htmlFor="url">URL</Label>
|
|
175
|
+
<Input id="url" name="url" value={url} onChange={(e) => setUrl(e.target.value)} required className="mt-1" placeholder="/about-us or https://example.com" />
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<Label htmlFor="language_id">Language</Label>
|
|
179
|
+
<Select
|
|
180
|
+
name="language_id"
|
|
181
|
+
value={languageId}
|
|
182
|
+
onValueChange={(val) => {
|
|
183
|
+
setLanguageId(val);
|
|
184
|
+
// Reset page and parent item selection if language changes, as they are language-specific
|
|
185
|
+
setPageId("");
|
|
186
|
+
setParentId("");
|
|
187
|
+
if (url.startsWith("/")) setUrl("#"); // Reset relative URL if it was page-linked
|
|
188
|
+
}}
|
|
189
|
+
required
|
|
190
|
+
disabled={isEditing && !!targetLanguageIdForTranslation} // Disable if creating a specific translation or editing
|
|
191
|
+
>
|
|
192
|
+
<SelectTrigger className="mt-1"><SelectValue placeholder="Select language" /></SelectTrigger>
|
|
193
|
+
<SelectContent>
|
|
194
|
+
{availableLanguages.map((lang) => (
|
|
195
|
+
<SelectItem key={lang.id} value={lang.id.toString()}>{lang.name} ({lang.code})</SelectItem>
|
|
196
|
+
))}
|
|
197
|
+
</SelectContent>
|
|
198
|
+
</Select>
|
|
199
|
+
</div>
|
|
200
|
+
<div>
|
|
201
|
+
<Label htmlFor="menu_key">Menu Location</Label>
|
|
202
|
+
<Select
|
|
203
|
+
name="menu_key"
|
|
204
|
+
value={menuKey}
|
|
205
|
+
defaultValue="HEADER"
|
|
206
|
+
onValueChange={(val) => {
|
|
207
|
+
setMenuKey(val as MenuLocation);
|
|
208
|
+
setParentId(""); // Reset parent if menu key changes
|
|
209
|
+
}}
|
|
210
|
+
required
|
|
211
|
+
disabled={isEditing && !!initialMenuKeyFromParam} // Disable if creating translation for specific menu
|
|
212
|
+
>
|
|
213
|
+
<SelectTrigger className="mt-1"><SelectValue placeholder="Select menu location" /></SelectTrigger>
|
|
214
|
+
<SelectContent>
|
|
215
|
+
{menuLocations.map((loc) => (
|
|
216
|
+
<SelectItem key={loc} value={loc}>{loc.charAt(0) + loc.slice(1).toLowerCase()}</SelectItem>
|
|
217
|
+
))}
|
|
218
|
+
</SelectContent>
|
|
219
|
+
</Select>
|
|
220
|
+
</div>
|
|
221
|
+
<div>
|
|
222
|
+
<Label htmlFor="order">Order</Label>
|
|
223
|
+
<Input id="order" name="order" type="number" value={order} onChange={(e) => setOrder(e.target.value)} required className="mt-1" />
|
|
224
|
+
</div>
|
|
225
|
+
<div>
|
|
226
|
+
<Label htmlFor="parent_id">Parent Item (Optional)</Label>
|
|
227
|
+
<Select name="parent_id" value={parentId} onValueChange={setParentId} disabled={!languageId || !menuKey}>
|
|
228
|
+
<SelectTrigger className="mt-1"><SelectValue placeholder="None (Top Level)" /></SelectTrigger>
|
|
229
|
+
<SelectContent>
|
|
230
|
+
<SelectItem value="___NONE___">None (Top Level)</SelectItem>
|
|
231
|
+
{availableParentItems
|
|
232
|
+
.filter(p => p.menu_key === menuKey)
|
|
233
|
+
.map((parent) => (
|
|
234
|
+
<SelectItem key={parent.id} value={parent.id.toString()}>{parent.label}</SelectItem>
|
|
235
|
+
))}
|
|
236
|
+
</SelectContent>
|
|
237
|
+
</Select>
|
|
238
|
+
<p className="text-xs text-muted-foreground mt-1">Parents must be in the same language and menu location.</p>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="flex justify-end space-x-3">
|
|
241
|
+
<Button type="button" variant="outline" onClick={() => router.push("/cms/navigation")} disabled={isPending}>Cancel</Button>
|
|
242
|
+
<Button type="submit" disabled={isPending || dataLoading || !languageId || !menuKey}>
|
|
243
|
+
{isPending ? "Saving..." : actionButtonText}
|
|
244
|
+
</Button>
|
|
245
|
+
</div>
|
|
246
|
+
</form>
|
|
247
|
+
);
|
|
248
|
+
}
|