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,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
+ }