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,362 @@
|
|
|
1
|
+
// app/cms/media/components/MediaUploadForm.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, useRef, useTransition, useEffect } from "react";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { Button } from "@nextblock-cms/ui";
|
|
7
|
+
import { Input } from "@nextblock-cms/ui";
|
|
8
|
+
import { Label } from "@nextblock-cms/ui";
|
|
9
|
+
import { Progress } from "@nextblock-cms/ui"; // Assuming you have this shadcn/ui component
|
|
10
|
+
import { UploadCloud, XCircle, CheckCircle2 } from "lucide-react";
|
|
11
|
+
import { recordMediaUpload } from "../actions"; // Server action
|
|
12
|
+
import type { Database } from "@nextblock-cms/db"; // Import Media type
|
|
13
|
+
|
|
14
|
+
type Media = Database['public']['Tables']['media']['Row'];
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
interface MediaUploadFormProps {
|
|
18
|
+
onUploadSuccess?: (newMedia: Media) => void;
|
|
19
|
+
// If true, the form expects recordMediaUpload to return data instead of redirecting.
|
|
20
|
+
// And will use onUploadSuccess instead of router.refresh().
|
|
21
|
+
returnJustData?: boolean;
|
|
22
|
+
defaultFolder?: string; // Optional pre-populated folder
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
import { useUploadFolder } from "../UploadFolderContext";
|
|
26
|
+
|
|
27
|
+
export default function MediaUploadForm({ onUploadSuccess, returnJustData, defaultFolder }: MediaUploadFormProps) {
|
|
28
|
+
const [isPending, startTransition] = useTransition();
|
|
29
|
+
const [file, setFile] = useState<File | null>(null);
|
|
30
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null); // For image preview
|
|
31
|
+
const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null); // For image dimensions
|
|
32
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
33
|
+
const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
|
|
34
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
35
|
+
const [isDraggingOver, setIsDraggingOver] = useState(false); // For drag-and-drop visual feedback
|
|
36
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
37
|
+
const [processingStatus, setProcessingStatus] = useState<"idle" | "processing" | "processed_error">("idle");
|
|
38
|
+
const { defaultFolder: ctxDefaultFolder } = useUploadFolder();
|
|
39
|
+
const [folder, setFolder] = useState<string>(defaultFolder || ctxDefaultFolder || "uploads/");
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setFolder(defaultFolder || ctxDefaultFolder || "uploads/");
|
|
43
|
+
}, [defaultFolder, ctxDefaultFolder]);
|
|
44
|
+
|
|
45
|
+
const resetFileSelection = () => {
|
|
46
|
+
setFile(null);
|
|
47
|
+
if (previewUrl) {
|
|
48
|
+
URL.revokeObjectURL(previewUrl);
|
|
49
|
+
}
|
|
50
|
+
setPreviewUrl(null);
|
|
51
|
+
setImageDimensions(null);
|
|
52
|
+
if (fileInputRef.current) {
|
|
53
|
+
fileInputRef.current.value = "";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const processFile = (selectedFile: File | undefined | null) => {
|
|
58
|
+
if (previewUrl) {
|
|
59
|
+
URL.revokeObjectURL(previewUrl); // Clean up previous preview
|
|
60
|
+
setPreviewUrl(null);
|
|
61
|
+
}
|
|
62
|
+
setImageDimensions(null); // Reset dimensions
|
|
63
|
+
|
|
64
|
+
if (selectedFile) {
|
|
65
|
+
setFile(selectedFile);
|
|
66
|
+
setUploadStatus("idle");
|
|
67
|
+
setUploadProgress(0);
|
|
68
|
+
setErrorMessage(null);
|
|
69
|
+
|
|
70
|
+
if (selectedFile.type.startsWith("image/")) {
|
|
71
|
+
const localPreviewUrl = URL.createObjectURL(selectedFile);
|
|
72
|
+
setPreviewUrl(localPreviewUrl);
|
|
73
|
+
|
|
74
|
+
// Get image dimensions
|
|
75
|
+
const img = new window.Image();
|
|
76
|
+
img.onload = () => {
|
|
77
|
+
setImageDimensions({ width: img.naturalWidth, height: img.naturalHeight });
|
|
78
|
+
URL.revokeObjectURL(img.src); // Clean up object URL used for dimensions
|
|
79
|
+
};
|
|
80
|
+
img.onerror = () => {
|
|
81
|
+
console.error("Error loading image to get dimensions.");
|
|
82
|
+
setImageDimensions(null);
|
|
83
|
+
URL.revokeObjectURL(img.src); // Clean up object URL used for dimensions
|
|
84
|
+
};
|
|
85
|
+
img.src = URL.createObjectURL(selectedFile); // Create a new object URL for dimension calculation
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
setFile(null); // Clear file if selection is cancelled or no file
|
|
89
|
+
// If previewUrl was set, it's already handled by the block at the start of this function
|
|
90
|
+
// or should be cleared if we are explicitly clearing the file.
|
|
91
|
+
if (previewUrl) {
|
|
92
|
+
URL.revokeObjectURL(previewUrl);
|
|
93
|
+
setPreviewUrl(null);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
+
const selectedFile = event.target.files?.[0];
|
|
100
|
+
processFile(selectedFile);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleDragEnter = (event: React.DragEvent<HTMLLabelElement>) => {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
event.stopPropagation();
|
|
106
|
+
setIsDraggingOver(true);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleDragOver = (event: React.DragEvent<HTMLLabelElement>) => {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
// You can add more checks here if needed, e.g., event.dataTransfer.types
|
|
113
|
+
setIsDraggingOver(true); // Ensure it stays true if dragging over children
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleDragLeave = (event: React.DragEvent<HTMLLabelElement>) => {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
event.stopPropagation();
|
|
119
|
+
// Check if the mouse is leaving the droppable area for real
|
|
120
|
+
// and not just moving over a child element.
|
|
121
|
+
// This can be tricky. A simpler approach is to rely on onDragEnter/onDrop to set it.
|
|
122
|
+
// For now, let's keep it simple:
|
|
123
|
+
setIsDraggingOver(false);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleDrop = (event: React.DragEvent<HTMLLabelElement>) => {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
event.stopPropagation();
|
|
129
|
+
setIsDraggingOver(false);
|
|
130
|
+
|
|
131
|
+
const droppedFiles = event.dataTransfer.files;
|
|
132
|
+
if (droppedFiles && droppedFiles.length > 0) {
|
|
133
|
+
// Process the first file, like in handleFileChange
|
|
134
|
+
const droppedFile = droppedFiles[0];
|
|
135
|
+
processFile(droppedFile);
|
|
136
|
+
// If you want to clear the file input after a drop (optional)
|
|
137
|
+
if (fileInputRef.current) {
|
|
138
|
+
fileInputRef.current.value = "";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const performUpload = async () => {
|
|
146
|
+
if (!file) {
|
|
147
|
+
setErrorMessage("Please select a file to upload.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (isPending || uploadStatus === "uploading" || processingStatus === "processing") {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setUploadStatus("uploading");
|
|
155
|
+
setUploadProgress(50); // Indicate start of upload
|
|
156
|
+
setErrorMessage(null);
|
|
157
|
+
setProcessingStatus("idle");
|
|
158
|
+
|
|
159
|
+
const currentFileForUpload = file;
|
|
160
|
+
|
|
161
|
+
startTransition(async () => {
|
|
162
|
+
try {
|
|
163
|
+
// 1. Upload file via proxy
|
|
164
|
+
const formData = new FormData();
|
|
165
|
+
formData.append('file', currentFileForUpload);
|
|
166
|
+
if (folder) {
|
|
167
|
+
formData.append('folder', folder);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const proxyResponse = await fetch('/api/upload/proxy', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
body: formData,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!proxyResponse.ok) {
|
|
176
|
+
const errorData = await proxyResponse.json();
|
|
177
|
+
throw new Error(errorData.error || 'Failed to upload file via proxy.');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { objectKey } = await proxyResponse.json();
|
|
181
|
+
setUploadProgress(100);
|
|
182
|
+
|
|
183
|
+
// 2. Record media in Supabase
|
|
184
|
+
// Derive a default alt/description for images when none provided
|
|
185
|
+
const deriveAltFromFilename = (name: string) => {
|
|
186
|
+
const lastDot = name.lastIndexOf('.');
|
|
187
|
+
const base = lastDot > 0 ? name.substring(0, lastDot) : name;
|
|
188
|
+
const spaced = base.replace(/[-+_\\]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
189
|
+
// Title-case words
|
|
190
|
+
return spaced.replace(/\b\w+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const defaultDescription = currentFileForUpload.type.startsWith('image/')
|
|
194
|
+
? deriveAltFromFilename(currentFileForUpload.name)
|
|
195
|
+
: undefined;
|
|
196
|
+
|
|
197
|
+
const mediaDataPayload = {
|
|
198
|
+
fileName: currentFileForUpload.name,
|
|
199
|
+
objectKey: objectKey,
|
|
200
|
+
fileType: currentFileForUpload.type,
|
|
201
|
+
sizeBytes: currentFileForUpload.size,
|
|
202
|
+
width: imageDimensions?.width,
|
|
203
|
+
height: imageDimensions?.height,
|
|
204
|
+
description: defaultDescription,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// 3. Process image variants
|
|
208
|
+
setProcessingStatus("processing");
|
|
209
|
+
const processResponse = await fetch('/api/process-image', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
objectKey: objectKey,
|
|
214
|
+
contentType: currentFileForUpload.type,
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const processData = await processResponse.json();
|
|
219
|
+
|
|
220
|
+
if (!processResponse.ok) {
|
|
221
|
+
console.error("Error processing image:", processData.error || "Failed to process image variants.");
|
|
222
|
+
setProcessingStatus("processed_error");
|
|
223
|
+
setErrorMessage(`Original uploaded, but variants failed: ${processData.error || "Unknown error"}`);
|
|
224
|
+
} else {
|
|
225
|
+
setProcessingStatus("idle");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 4. Record media in Supabase with variant info
|
|
229
|
+
const finalMediaPayload = {
|
|
230
|
+
...mediaDataPayload,
|
|
231
|
+
r2OriginalKey: objectKey,
|
|
232
|
+
r2Variants: processData.processedVariants || [],
|
|
233
|
+
originalImageDetails: processData.originalImage,
|
|
234
|
+
blurDataURL: processData.blurDataURL || null,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const recordResult = await recordMediaUpload(finalMediaPayload, returnJustData);
|
|
238
|
+
|
|
239
|
+
const handleSuccess = (newMedia?: Media) => {
|
|
240
|
+
setUploadStatus("success");
|
|
241
|
+
if (returnJustData && newMedia) {
|
|
242
|
+
onUploadSuccess?.(newMedia);
|
|
243
|
+
}
|
|
244
|
+
// Reset form state
|
|
245
|
+
resetFileSelection();
|
|
246
|
+
if (processingStatus !== "processed_error") {
|
|
247
|
+
setProcessingStatus("idle");
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (returnJustData) {
|
|
252
|
+
if (recordResult && 'success' in recordResult && recordResult.success && recordResult.data) {
|
|
253
|
+
handleSuccess(recordResult.data);
|
|
254
|
+
} else {
|
|
255
|
+
throw new Error((recordResult && 'error' in recordResult && recordResult.error) || "Media record action failed.");
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
handleSuccess();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
} catch (err: unknown) {
|
|
262
|
+
const isRedirect = (err instanceof Error && err.message === 'NEXT_REDIRECT') || (typeof (err as any)?.digest === 'string' && (err as any).digest.startsWith('NEXT_REDIRECT'));
|
|
263
|
+
|
|
264
|
+
if (isRedirect && !returnJustData) {
|
|
265
|
+
setUploadStatus("success");
|
|
266
|
+
resetFileSelection();
|
|
267
|
+
} else {
|
|
268
|
+
console.error("Upload process error:", err);
|
|
269
|
+
setUploadStatus("error");
|
|
270
|
+
setErrorMessage(err instanceof Error ? err.message : "An unknown error occurred during upload.");
|
|
271
|
+
setUploadProgress(0);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
performUpload();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div className="p-6 border rounded-lg shadow-sm bg-card mb-6">
|
|
284
|
+
<form onSubmit={handleFormSubmit} className="space-y-4">
|
|
285
|
+
<div>
|
|
286
|
+
<Label htmlFor="media-file" className="text-base font-medium">Upload New Media</Label>
|
|
287
|
+
<div className="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
288
|
+
<div>
|
|
289
|
+
<Label htmlFor="media-folder" className="text-sm">Folder (e.g., uploads/images/)</Label>
|
|
290
|
+
<Input
|
|
291
|
+
id="media-folder"
|
|
292
|
+
placeholder="uploads/"
|
|
293
|
+
value={folder}
|
|
294
|
+
onChange={(e) => setFolder(e.target.value)}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="mt-2 flex items-center justify-center w-full">
|
|
299
|
+
<label
|
|
300
|
+
htmlFor="media-file-input"
|
|
301
|
+
className={`flex flex-col items-center justify-center w-full h-40 border-2 border-dashed rounded-lg cursor-pointer bg-muted/30 hover:bg-muted/50 transition-colors ${
|
|
302
|
+
isDraggingOver ? "border-primary bg-primary-foreground/20" : "border-input"
|
|
303
|
+
}`}
|
|
304
|
+
onDrop={handleDrop}
|
|
305
|
+
onDragOver={handleDragOver}
|
|
306
|
+
onDragEnter={handleDragEnter}
|
|
307
|
+
onDragLeave={handleDragLeave}
|
|
308
|
+
>
|
|
309
|
+
<div className="flex flex-col items-center justify-center pt-5 pb-6 pointer-events-none"> {/* pointer-events-none for children */}
|
|
310
|
+
<UploadCloud className="w-10 h-10 mb-3 text-muted-foreground" />
|
|
311
|
+
<p className="mb-2 text-sm text-muted-foreground">
|
|
312
|
+
<span className="font-semibold">Click to upload</span> or drag and drop
|
|
313
|
+
</p>
|
|
314
|
+
<p className="text-xs text-muted-foreground">SVG, PNG, JPG, GIF, MP4, PDF (MAX. 10MB)</p>
|
|
315
|
+
</div>
|
|
316
|
+
<Input id="media-file-input" type="file" className="hidden" onChange={handleFileChange} ref={fileInputRef} />
|
|
317
|
+
</label>
|
|
318
|
+
</div>
|
|
319
|
+
{previewUrl && file && file.type.startsWith("image/") && (
|
|
320
|
+
<div className="mt-4">
|
|
321
|
+
<Label>Preview:</Label>
|
|
322
|
+
<Image src={previewUrl} alt="Preview" width={300} height={192} className="mt-2 rounded-md max-h-48 w-auto object-contain border" />
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
{file && <p className="text-sm mt-2 text-muted-foreground">Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)</p>}
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{uploadStatus === "uploading" && (
|
|
329
|
+
<Progress value={uploadProgress} className="w-full h-2" />
|
|
330
|
+
)}
|
|
331
|
+
{uploadStatus === "success" && (
|
|
332
|
+
<div className="flex items-center text-green-600">
|
|
333
|
+
<CheckCircle2 className="h-5 w-5 mr-2" />
|
|
334
|
+
<p>Upload successful!</p>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
{uploadStatus === "error" && errorMessage && (
|
|
338
|
+
<div className="flex items-center text-red-600">
|
|
339
|
+
<XCircle className="h-5 w-5 mr-2" />
|
|
340
|
+
<p>Error: {errorMessage}</p>
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
{processingStatus === "processing" && (
|
|
344
|
+
<p className="text-sm text-blue-600 animate-pulse">Processing image variants...</p>
|
|
345
|
+
)}
|
|
346
|
+
{/* Message for when original uploads but variants fail, errorMessage will be set */}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
<Button
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={performUpload}
|
|
352
|
+
disabled={isPending || uploadStatus === "uploading" || processingStatus === "processing" || !file}
|
|
353
|
+
className="w-full sm:w-auto"
|
|
354
|
+
>
|
|
355
|
+
{uploadStatus === "uploading" ? `Uploading ${uploadProgress}%...`
|
|
356
|
+
: processingStatus === "processing" ? "Processing..."
|
|
357
|
+
: "Upload File"}
|
|
358
|
+
</Button>
|
|
359
|
+
</form>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// app/cms/media/page.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
// import Link from "next/link"; // Unused, MediaGridClient handles item links
|
|
5
|
+
import type { Database } from "@nextblock-cms/db";
|
|
6
|
+
// DropdownMenu related imports are now handled within MediaGridClient or its sub-components if needed individually.
|
|
7
|
+
|
|
8
|
+
type Media = Database['public']['Tables']['media']['Row'];
|
|
9
|
+
// If page.tsx itself doesn't directly use DropdownMenu, these can be removed from here.
|
|
10
|
+
// For now, assuming MediaGridClient handles its own dropdowns.
|
|
11
|
+
import MediaUploadForm from "./components/MediaUploadForm";
|
|
12
|
+
// MediaImage and DeleteMediaButtonClient are used by MediaGridClient, not directly here anymore.
|
|
13
|
+
import MediaGridClient from "./components/MediaGridClient"; // Import the new client component
|
|
14
|
+
import FolderNavigator from "./components/FolderNavigator";
|
|
15
|
+
|
|
16
|
+
async function getMediaItems(folder?: string, folderPrefix?: string, search?: string): Promise<Media[]> {
|
|
17
|
+
const supabase = createClient();
|
|
18
|
+
let query = supabase
|
|
19
|
+
.from("media")
|
|
20
|
+
.select("*")
|
|
21
|
+
.order("created_at", { ascending: false });
|
|
22
|
+
|
|
23
|
+
if (folder && folder.trim()) {
|
|
24
|
+
query = query.eq('folder', folder);
|
|
25
|
+
} else if (folderPrefix && folderPrefix.trim()) {
|
|
26
|
+
query = query.ilike('folder', `${folderPrefix}%`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (search && search.trim()) {
|
|
30
|
+
const term = search.trim();
|
|
31
|
+
query = query.or(`file_name.ilike.%${term}%,description.ilike.%${term}%`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { data, error } = await query;
|
|
35
|
+
|
|
36
|
+
if (error) {
|
|
37
|
+
console.error("Error fetching media items:", error);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return data || [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getDistinctFolders(search?: string): Promise<string[]> {
|
|
44
|
+
const supabase = createClient();
|
|
45
|
+
const { data, error } = await supabase
|
|
46
|
+
.from("media")
|
|
47
|
+
.select("folder")
|
|
48
|
+
.order("folder", { ascending: true });
|
|
49
|
+
if (error) {
|
|
50
|
+
console.error("Error fetching folders:", error);
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
let folders = (data || [])
|
|
54
|
+
.map((r: any) => r.folder)
|
|
55
|
+
.filter((f: any) => typeof f === 'string' && f.length > 0);
|
|
56
|
+
if (search && search.trim()) {
|
|
57
|
+
const t = search.trim().toLowerCase();
|
|
58
|
+
folders = folders.filter((f: string) => f.toLowerCase().includes(t));
|
|
59
|
+
}
|
|
60
|
+
// Ensure trailing slash for consistency
|
|
61
|
+
return Array.from(new Set(folders.map((f: string) => (f.endsWith('/') ? f : f + '/'))));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function getFolderCounts(): Promise<Record<string, number>> {
|
|
65
|
+
const supabase = createClient();
|
|
66
|
+
const { data, error } = await supabase
|
|
67
|
+
.from("media")
|
|
68
|
+
.select("folder");
|
|
69
|
+
if (error) {
|
|
70
|
+
console.error("Error fetching folder counts:", error);
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
const counts: Record<string, number> = {};
|
|
74
|
+
(data || []).forEach((row: any) => {
|
|
75
|
+
const f: string | null = row.folder;
|
|
76
|
+
if (!f || typeof f !== 'string' || f.length === 0) return;
|
|
77
|
+
const norm = f.endsWith('/') ? f : `${f}/`;
|
|
78
|
+
// accumulate counts for each prefix in the path
|
|
79
|
+
const parts = norm.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
80
|
+
let prefix = '';
|
|
81
|
+
for (let i = 0; i < parts.length; i++) {
|
|
82
|
+
prefix += (i === 0 ? '' : '/') + parts[i];
|
|
83
|
+
const key = `${prefix}/`;
|
|
84
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return counts;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
91
|
+
|
|
92
|
+
export default async function CmsMediaLibraryPage(props: { searchParams?: Promise<{ folder?: string; folderPrefix?: string; q?: string }> }) {
|
|
93
|
+
const searchParams = (await props.searchParams) || {};
|
|
94
|
+
const selectedFolder = searchParams.folder;
|
|
95
|
+
const selectedFolderPrefix = searchParams.folderPrefix;
|
|
96
|
+
const searchQuery = searchParams.q;
|
|
97
|
+
const [mediaItems, folders, folderCounts] = await Promise.all([
|
|
98
|
+
getMediaItems(selectedFolder, selectedFolderPrefix, searchQuery),
|
|
99
|
+
getDistinctFolders(searchQuery),
|
|
100
|
+
getFolderCounts(),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="w-full max-w-screen-2xl mx-auto px-4 overflow-x-hidden space-y-6">
|
|
105
|
+
<div className="flex justify-between items-center">
|
|
106
|
+
<h1 className="text-2xl font-semibold">Media Library</h1>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<MediaUploadForm />
|
|
110
|
+
|
|
111
|
+
{/* Compact folder navigator with top tabs and subfolder pills */}
|
|
112
|
+
<div className="mt-2">
|
|
113
|
+
<FolderNavigator basePath="/cms/media" folders={folders} selectedFolder={selectedFolder || ''} selectedPrefix={selectedFolderPrefix || ''} counts={folderCounts} searchTerm={searchQuery || ''} />
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* The media grid and empty state are now handled by MediaGridClient */}
|
|
117
|
+
<MediaGridClient initialMediaItems={mediaItems} r2BaseUrl={R2_BASE_URL} />
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// app/cms/navigation/[id]/edit/page.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import NavigationItemForm from "../../components/NavigationItemForm";
|
|
5
|
+
import { updateNavigationItem } from "../../actions";
|
|
6
|
+
import { getLanguages, getNavigationItems, getPages } from "../../utils";
|
|
7
|
+
import type { Database } from "@nextblock-cms/db";
|
|
8
|
+
import { notFound, redirect } from "next/navigation";
|
|
9
|
+
|
|
10
|
+
type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
import { Button } from "@nextblock-cms/ui";
|
|
13
|
+
import { ArrowLeft } from "lucide-react";
|
|
14
|
+
import NavigationLanguageSwitcher from "../../components/NavigationLanguageSwitcher";
|
|
15
|
+
|
|
16
|
+
async function getNavigationItemData(id: number): Promise<NavigationItem | null> {
|
|
17
|
+
const supabase = createClient();
|
|
18
|
+
const { data, error } = await supabase
|
|
19
|
+
.from("navigation_items")
|
|
20
|
+
.select("*") // Ensure translation_group_id is selected
|
|
21
|
+
.eq("id", id)
|
|
22
|
+
.single();
|
|
23
|
+
|
|
24
|
+
if (error) {
|
|
25
|
+
console.error("Error fetching navigation item for edit:", error);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async function EditNavigationItemPage(props: { params: Promise<{ id: string }> }) {
|
|
32
|
+
const params = await props.params;
|
|
33
|
+
const itemId = parseInt(params.id, 10);
|
|
34
|
+
if (isNaN(itemId)) {
|
|
35
|
+
return notFound();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Admin check
|
|
39
|
+
const supabase = createClient();
|
|
40
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
41
|
+
if (!user) return redirect(`/sign-in?redirect=/cms/navigation/${itemId}/edit`);
|
|
42
|
+
|
|
43
|
+
const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single();
|
|
44
|
+
if (profile?.role !== 'ADMIN') {
|
|
45
|
+
return <div className="p-6 text-center text-red-500">Access Denied. Admin privileges required.</div>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [item, allLanguages, allNavItems, allPages] = await Promise.all([
|
|
49
|
+
getNavigationItemData(itemId),
|
|
50
|
+
getLanguages(),
|
|
51
|
+
getNavigationItems(),
|
|
52
|
+
getPages(),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
if (!item) {
|
|
56
|
+
return notFound();
|
|
57
|
+
}
|
|
58
|
+
if (!item.translation_group_id) {
|
|
59
|
+
// This case should ideally not happen if all items get a translation_group_id upon creation.
|
|
60
|
+
// Handle gracefully, perhaps by redirecting or showing an error.
|
|
61
|
+
console.error(`Navigation item ${item.id} is missing a translation_group_id.`);
|
|
62
|
+
// For now, let's allow editing but the switcher might not work as expected.
|
|
63
|
+
// Or, redirect to a page that explains the issue / offers to assign one.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
const updateItemWithId = updateNavigationItem.bind(null, itemId);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="max-w-2xl mx-auto">
|
|
71
|
+
<div className="mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
72
|
+
<div className="flex items-center gap-3">
|
|
73
|
+
<Button variant="outline" size="icon" aria-label="Back to navigation items" asChild>
|
|
74
|
+
<Link href="/cms/navigation">
|
|
75
|
+
<ArrowLeft className="h-4 w-4" />
|
|
76
|
+
</Link>
|
|
77
|
+
</Button>
|
|
78
|
+
<div>
|
|
79
|
+
<h1 className="text-2xl font-semibold">Edit Navigation Item</h1>
|
|
80
|
+
<p className="text-sm text-muted-foreground truncate max-w-xs" title={item.label}>{item.label}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
{item.translation_group_id && allLanguages.length > 0 && (
|
|
84
|
+
<NavigationLanguageSwitcher
|
|
85
|
+
currentItem={item}
|
|
86
|
+
allSiteLanguages={allLanguages}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<NavigationItemForm
|
|
91
|
+
item={item}
|
|
92
|
+
formAction={updateItemWithId}
|
|
93
|
+
actionButtonText="Update Item"
|
|
94
|
+
isEditing={true}
|
|
95
|
+
languages={allLanguages}
|
|
96
|
+
parentItems={allNavItems}
|
|
97
|
+
pages={allPages}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|