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,419 @@
1
+ // app/cms/posts/components/PostForm.tsx
2
+ "use client";
3
+
4
+ import { useEffect, useState, useTransition, useCallback } from "react";
5
+ import { useRouter, useSearchParams } from "next/navigation";
6
+ import Image from "next/image";
7
+ import { Button } from "@nextblock-cms/ui";
8
+ import { Input } from "@nextblock-cms/ui";
9
+ import { Label } from "@nextblock-cms/ui";
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "@nextblock-cms/ui";
17
+ import { Textarea } from "@nextblock-cms/ui";
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogHeader,
22
+ DialogTitle,
23
+ DialogTrigger,
24
+ DialogFooter,
25
+ DialogClose,
26
+ } from "@nextblock-cms/ui";
27
+ import type { Database } from "@nextblock-cms/db";
28
+ import { useAuth } from "@/context/AuthContext";
29
+
30
+ type Post = Database['public']['Tables']['posts']['Row'];
31
+ type PageStatus = Database['public']['Enums']['page_status'];
32
+ type Language = Database['public']['Tables']['languages']['Row'];
33
+ type Media = Database['public']['Tables']['media']['Row'];
34
+ // import MediaGridClient from "@/app/cms/media/components/MediaGridClient"; // Will render a custom grid instead
35
+ import MediaImage from "@/app/cms/media/components/MediaImage"; // For displaying images in the modal
36
+ import { getMediaItems } from "@/app/cms/media/actions";
37
+ import MediaUploadForm from "@/app/cms/media/components/MediaUploadForm";
38
+ import { Separator } from "@nextblock-cms/ui";
39
+
40
+
41
+ interface PostFormProps {
42
+ post?: Post & { feature_image_id?: string | null }; // Assuming feature_image_id can be string
43
+ formAction: (formData: FormData) => Promise<{ error?: string } | void>;
44
+ actionButtonText?: string;
45
+ isEditing?: boolean;
46
+ availableLanguagesProp?: Language[]; // Make optional
47
+ initialFeatureImageUrl?: string | null;
48
+ initialFeatureImageId?: string | null; // Pass initial ID as string
49
+ }
50
+
51
+ export default function PostForm({
52
+ post,
53
+ formAction,
54
+ actionButtonText = "Save Post",
55
+ isEditing = false,
56
+ availableLanguagesProp = [], // Default to empty array
57
+ initialFeatureImageUrl,
58
+ initialFeatureImageId,
59
+ }: PostFormProps) {
60
+ const router = useRouter();
61
+ const searchParams = useSearchParams();
62
+ const [isPending, startTransition] = useTransition();
63
+ const { user, isLoading: authLoading } = useAuth();
64
+
65
+ const [title, setTitle] = useState(post?.title || "");
66
+ const [slug, setSlug] = useState(post?.slug || "");
67
+ const [languageId, setLanguageId] = useState<string>(
68
+ post?.language_id?.toString() || ""
69
+ );
70
+ const [status, setStatus] = useState<PageStatus>(post?.status || "draft");
71
+ const [excerpt, setExcerpt] = useState(post?.excerpt || "");
72
+ const [publishedAt, setPublishedAt] = useState<string>(() => {
73
+ if (post?.published_at) {
74
+ try {
75
+ const date = new Date(post.published_at);
76
+ const year = date.getFullYear();
77
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
78
+ const day = date.getDate().toString().padStart(2, '0');
79
+ const hours = date.getHours().toString().padStart(2, '0');
80
+ const minutes = date.getMinutes().toString().padStart(2, '0');
81
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
82
+ } catch {
83
+ return "";
84
+ }
85
+ }
86
+ return "";
87
+ });
88
+ const [metaTitle, setMetaTitle] = useState(post?.meta_title || "");
89
+ const [metaDescription, setMetaDescription] = useState(
90
+ post?.meta_description || ""
91
+ );
92
+
93
+ // Use the passed-in languages directly
94
+ const [availableLanguages] = useState<Language[]>(availableLanguagesProp);
95
+
96
+ const [selectedFeatureImage, setSelectedFeatureImage] = useState<{ id: string | null; url: string | null }>({
97
+ id: initialFeatureImageId || post?.feature_image_id || null, // Prioritize prop, then post data
98
+ url: initialFeatureImageUrl || null,
99
+ });
100
+ const [isModalOpen, setIsModalOpen] = useState(false);
101
+ const [mediaItems, setMediaItems] = useState<Media[]>([]);
102
+ const [mediaLoading, setMediaLoading] = useState(false);
103
+ const [mediaError, setMediaError] = useState<string | null>(null);
104
+ const [mediaPage, setMediaPage] = useState(1);
105
+ const [hasMoreMedia, setHasMoreMedia] = useState(true);
106
+
107
+ const [formMessage, setFormMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
108
+
109
+ useEffect(() => {
110
+ // Update selectedFeatureImage if initial props change
111
+ setSelectedFeatureImage({
112
+ id: initialFeatureImageId || post?.feature_image_id || null,
113
+ url: initialFeatureImageUrl || null,
114
+ });
115
+ }, [initialFeatureImageId, initialFeatureImageUrl, post?.feature_image_id]);
116
+
117
+ const loadMedia = useCallback(async (pageToLoad = 1, append = false) => {
118
+ if (!hasMoreMedia && append && pageToLoad > mediaPage) return;
119
+ setMediaLoading(true);
120
+ setMediaError(null);
121
+ try {
122
+ const result = await getMediaItems(pageToLoad, 20); // Fetch 20 items per page
123
+ if (result.error) {
124
+ setMediaError(result.error);
125
+ if (!append) setMediaItems([]); // Clear if not appending on error
126
+ } else if (result.data) {
127
+ setMediaItems(prev => append ? [...prev, ...(result.data || [])] : (result.data || []));
128
+ setHasMoreMedia(result.hasMore !== undefined ? result.hasMore : false);
129
+ setMediaPage(pageToLoad);
130
+ }
131
+ } catch {
132
+ setMediaError("An unexpected error occurred while fetching media.");
133
+ if (!append) setMediaItems([]);
134
+ } finally {
135
+ setMediaLoading(false);
136
+ }
137
+ }, [hasMoreMedia, mediaPage]);
138
+
139
+ // Load initial media when modal is opened
140
+ useEffect(() => {
141
+ if (isModalOpen) {
142
+ // Reset and load fresh if opening modal, or if mediaItems is empty
143
+ if (mediaItems.length === 0 || !hasMoreMedia || mediaPage !==1) {
144
+ setMediaPage(1);
145
+ setHasMoreMedia(true); // Assume there might be more media on fresh open
146
+ loadMedia(1, false);
147
+ }
148
+ }
149
+ }, [isModalOpen, hasMoreMedia, loadMedia, mediaItems.length, mediaPage]);
150
+
151
+ const handleImageSelectInModal = (image: Media) => {
152
+ const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
153
+ if (!r2BaseUrl) {
154
+ console.error("NEXT_PUBLIC_R2_PUBLIC_URL is not set. Cannot construct image URL.");
155
+ setMediaError("Image server configuration is missing. Cannot display images.");
156
+ return;
157
+ }
158
+ const imageUrl = image.object_key ? `${r2BaseUrl}/${image.object_key}` : null;
159
+
160
+ if (!imageUrl) {
161
+ console.error("Selected image does not have an object_key:", image);
162
+ setMediaError("Selected image is missing a valid identifier.");
163
+ return;
164
+ }
165
+
166
+ setSelectedFeatureImage({ id: image.id, url: imageUrl }); // image.id is already string (uuid)
167
+ setIsModalOpen(false);
168
+ };
169
+
170
+
171
+ useEffect(() => {
172
+ const successMessage = searchParams.get('success');
173
+ const errorMessage = searchParams.get('error');
174
+ if (successMessage) {
175
+ setFormMessage({ type: 'success', text: decodeURIComponent(successMessage) });
176
+ } else if (errorMessage) {
177
+ setFormMessage({ type: 'error', text: decodeURIComponent(errorMessage) });
178
+ }
179
+ }, [searchParams]);
180
+
181
+ // Initialize languageId if creating new post and languages are available
182
+ useEffect(() => {
183
+ if (!isEditing && availableLanguages.length > 0 && !languageId) { // check !isEditing too
184
+ const defaultLang = availableLanguages.find(l => l.is_default) || availableLanguages[0];
185
+ if (defaultLang) {
186
+ setLanguageId(defaultLang.id.toString());
187
+ }
188
+ }
189
+ }, [isEditing, availableLanguages, languageId]); // Add isEditing to dependency array
190
+
191
+
192
+ const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
193
+ const newTitle = e.target.value;
194
+ setTitle(newTitle);
195
+ if (!isEditing || !slug) { // Only auto-generate slug if creating new or slug is empty
196
+ setSlug(newTitle.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, ""));
197
+ }
198
+ };
199
+
200
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
201
+ event.preventDefault();
202
+ setFormMessage(null);
203
+ const formData = new FormData(event.currentTarget);
204
+
205
+ startTransition(async () => {
206
+ const result = await formAction(formData);
207
+ if (result?.error) {
208
+ setFormMessage({ type: 'error', text: result.error });
209
+ }
210
+ // Success is handled by redirect with query param in server action
211
+ });
212
+ };
213
+
214
+ // Remove languagesLoading from this condition
215
+ if (authLoading) {
216
+ return <div>Loading form...</div>;
217
+ }
218
+ if (!user) {
219
+ return <div>Please log in to manage posts.</div>;
220
+ }
221
+
222
+ return (
223
+ <form onSubmit={handleSubmit} className="space-y-6 w-full mx-auto px-6">
224
+ {formMessage && (
225
+ <div
226
+ className={`p-3 rounded-md text-sm ${
227
+ formMessage.type === 'success'
228
+ ? 'bg-green-100 text-green-700 border border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700'
229
+ : 'bg-red-100 text-red-700 border border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-700'
230
+ }`}
231
+ >
232
+ {formMessage.text}
233
+ </div>
234
+ )}
235
+ <div>
236
+ <Label htmlFor="title">Title</Label>
237
+ <Input id="title" name="title" value={title} onChange={handleTitleChange} required className="mt-1" />
238
+ </div>
239
+
240
+ <div>
241
+ <Label htmlFor="slug">Slug</Label>
242
+ <Input id="slug" name="slug" value={slug} onChange={(e) => setSlug(e.target.value)} required className="mt-1" />
243
+ </div>
244
+
245
+ <div>
246
+ <Label htmlFor="language_id">Language</Label>
247
+ {availableLanguages.length > 0 ? (
248
+ <Select name="language_id" value={languageId} onValueChange={setLanguageId} required disabled={isEditing}>
249
+ <SelectTrigger className="mt-1"><SelectValue placeholder="Select language" /></SelectTrigger>
250
+ <SelectContent>
251
+ {availableLanguages.map((lang) => (
252
+ <SelectItem key={lang.id} value={lang.id.toString()}>{lang.name} ({lang.code})</SelectItem>
253
+ ))}
254
+ </SelectContent>
255
+ </Select>
256
+ ) : (
257
+ <p className="text-sm text-muted-foreground mt-1">No languages available. Please add languages in CMS settings.</p>
258
+ )}
259
+ </div>
260
+
261
+ <div>
262
+ <Label htmlFor="excerpt">Excerpt</Label>
263
+ <Textarea id="excerpt" name="excerpt" value={excerpt} onChange={(e) => setExcerpt(e.target.value)} className="mt-1" rows={3} />
264
+ </div>
265
+
266
+ <div>
267
+ <Label htmlFor="status">Status</Label>
268
+ <Select name="status" value={status} onValueChange={(value) => setStatus(value as PageStatus)} required>
269
+ <SelectTrigger className="mt-1"><SelectValue placeholder="Select status" /></SelectTrigger>
270
+ <SelectContent>
271
+ <SelectItem value="draft">Draft</SelectItem>
272
+ <SelectItem value="published">Published</SelectItem>
273
+ <SelectItem value="archived">Archived</SelectItem>
274
+ </SelectContent>
275
+ </Select>
276
+ </div>
277
+
278
+ <div>
279
+ <Label htmlFor="published_at">Published At (Optional)</Label>
280
+ <Input
281
+ id="published_at"
282
+ name="published_at"
283
+ type="datetime-local"
284
+ value={publishedAt}
285
+ onChange={(e) => setPublishedAt(e.target.value)}
286
+ className="mt-1"
287
+ />
288
+ <p className="text-xs text-muted-foreground mt-1">Leave blank to publish immediately when status is &apos;Published&apos;.</p>
289
+ </div>
290
+
291
+ <div>
292
+ <Label htmlFor="meta_title">Meta Title (SEO)</Label>
293
+ <Input id="meta_title" name="meta_title" value={metaTitle} onChange={(e) => setMetaTitle(e.target.value)} className="mt-1" />
294
+ </div>
295
+
296
+ <div>
297
+ <Label htmlFor="meta_description">Meta Description (SEO)</Label>
298
+ <Textarea id="meta_description" name="meta_description" value={metaDescription} onChange={(e) => setMetaDescription(e.target.value)} className="mt-1" rows={3} />
299
+ </div>
300
+
301
+ {/* Feature Image Selection */}
302
+ <div>
303
+ <Label htmlFor="feature_image">Feature Image</Label>
304
+ <Input type="hidden" name="feature_image_id" value={selectedFeatureImage.id || ""} />
305
+ <div className="mt-2">
306
+ {selectedFeatureImage.url && (
307
+ <div className="mb-4">
308
+ <Image
309
+ src={selectedFeatureImage.url}
310
+ alt="Selected feature image"
311
+ width={200}
312
+ height={200}
313
+ className="rounded-md object-cover"
314
+ />
315
+ <Button
316
+ type="button"
317
+ variant="link"
318
+ className="mt-2 text-red-600 px-0"
319
+ onClick={() => setSelectedFeatureImage({ id: null, url: null })}
320
+ >
321
+ Remove Image
322
+ </Button>
323
+ </div>
324
+ )}
325
+ <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
326
+ <DialogTrigger asChild>
327
+ <Button type="button" variant="outline">
328
+ {selectedFeatureImage.id ? "Change Feature Image" : "Select Feature Image"}
329
+ </Button>
330
+ </DialogTrigger>
331
+ <DialogContent className="sm:max-w-[90vw] max-h-[90vh] flex flex-col">
332
+ <DialogHeader>
333
+ <DialogTitle>Select Feature Image</DialogTitle>
334
+ </DialogHeader>
335
+ <div className="p-1">
336
+ <MediaUploadForm
337
+ returnJustData={true}
338
+ defaultFolder={`posts/${(slug || 'untitled').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '')}/`}
339
+ onUploadSuccess={(newlyUploadedMedia) => {
340
+ setMediaItems(prevItems => [newlyUploadedMedia, ...prevItems.filter(item => item.id !== newlyUploadedMedia.id)]);
341
+ handleImageSelectInModal(newlyUploadedMedia);
342
+ }}
343
+ />
344
+ </div>
345
+ <Separator className="my-4" />
346
+ <div className="py-4 flex-grow overflow-y-auto" id="media-modal-scroll-area">
347
+ {mediaLoading && mediaItems.length === 0 && <p className="text-center text-muted-foreground">Loading media...</p>}
348
+ {mediaError && <p className="text-red-600 text-center">{mediaError}</p>}
349
+ {!mediaLoading && !mediaError && mediaItems.length === 0 && <p className="text-center text-muted-foreground">No media items found. Try uploading some first.</p>}
350
+
351
+ {mediaItems.length > 0 && (
352
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 gap-3">
353
+ {mediaItems.map((item) => {
354
+ const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
355
+ if (!r2BaseUrl && item.object_key) {
356
+ // This check is more for safety, error primarily handled in handleImageSelectInModal
357
+ if (!mediaError) setMediaError("Image server configuration is missing. Cannot display images.");
358
+ return null; // Or a placeholder
359
+ }
360
+ const imageUrl = item.object_key ? `${r2BaseUrl}/${item.object_key}` : null;
361
+
362
+ // Only render image-type media for selection
363
+ if (!item.file_type?.startsWith("image/") || !imageUrl) {
364
+ return null;
365
+ }
366
+
367
+ return (
368
+ <div
369
+ key={item.id}
370
+ className="group relative border rounded-lg overflow-hidden shadow-sm aspect-square bg-muted/20 transition-all cursor-pointer hover:ring-2 hover:ring-primary"
371
+ onClick={() => handleImageSelectInModal(item)}
372
+ onKeyDown={(e) => e.key === 'Enter' && handleImageSelectInModal(item)}
373
+ tabIndex={0}
374
+ role="button"
375
+ aria-label={`Select ${item.file_name}`}
376
+ >
377
+ <MediaImage
378
+ src={imageUrl}
379
+ alt={item.description || item.file_name}
380
+ width={item.width || 300} // Provide a fallback or ensure width is always present
381
+ height={item.height || 300} // Provide a fallback or ensure height is always present
382
+ blurDataURL={item.blur_data_url}
383
+ className="h-full w-full object-cover transition-transform group-hover:scale-105"
384
+ />
385
+ <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-2">
386
+ <p className="text-xs text-white truncate" title={item.file_name}>{item.file_name}</p>
387
+ </div>
388
+ </div>
389
+ );
390
+ })}
391
+ </div>
392
+ )}
393
+ {!mediaLoading && hasMoreMedia && mediaItems.length > 0 && (
394
+ <div className="text-center mt-6">
395
+ <Button onClick={() => loadMedia(mediaPage + 1, true)} variant="outline" disabled={mediaLoading}>
396
+ {mediaLoading ? "Loading..." : "Load More"}
397
+ </Button>
398
+ </div>
399
+ )}
400
+ </div>
401
+ <DialogFooter className="mt-auto pt-4 border-t">
402
+ <DialogClose asChild>
403
+ <Button type="button" variant="outline" onClick={() => { setMediaError(null); }}>Cancel</Button>
404
+ </DialogClose>
405
+ </DialogFooter>
406
+ </DialogContent>
407
+ </Dialog>
408
+ </div>
409
+ </div>
410
+
411
+ <div className="flex justify-end space-x-3 pt-6"> {/* Increased pt for spacing */}
412
+ <Button type="button" variant="outline" onClick={() => router.push("/cms/posts")} disabled={isPending}>Cancel</Button>
413
+ <Button type="submit" disabled={isPending || authLoading || availableLanguages.length === 0}>
414
+ {isPending ? "Saving..." : actionButtonText}
415
+ </Button>
416
+ </div>
417
+ </form>
418
+ );
419
+ }
@@ -0,0 +1,21 @@
1
+ // app/cms/posts/new/page.tsx
2
+ import PostForm from "../components/PostForm";
3
+ import { createPost } from "../actions";
4
+ import { getLanguages } from "@/app/cms/settings/languages/actions";
5
+
6
+ export default async function NewPostPage() {
7
+ const languagesResult = await getLanguages();
8
+ const allLanguages = languagesResult.data || []; // Ensure it's an array
9
+
10
+ return (
11
+ <div className="max-w-2xl mx-auto">
12
+ <h1 className="text-2xl font-bold mb-6">Create New Post</h1>
13
+ <PostForm
14
+ formAction={createPost}
15
+ actionButtonText="Create Post"
16
+ isEditing={false}
17
+ availableLanguagesProp={allLanguages}
18
+ />
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,192 @@
1
+ // app/cms/posts/page.tsx
2
+ import React from "react";
3
+ import { createClient } from "@nextblock-cms/db/server";
4
+ import Link from "next/link";
5
+ import { Button } from "@nextblock-cms/ui";
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "@nextblock-cms/ui";
14
+ import { Badge } from "@nextblock-cms/ui";
15
+ import { MoreHorizontal, PlusCircle, Edit3, PenTool } from "lucide-react"; // Removed Trash2 as it's in the client component
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuTrigger,
21
+ DropdownMenuSeparator,
22
+ } from "@nextblock-cms/ui";
23
+ // deletePost server action is now used by DeletePostButtonClient
24
+ import type { Database } from "@nextblock-cms/db";
25
+ import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
26
+
27
+ type Post = Database['public']['Tables']['posts']['Row'] & { feature_image_url?: string | null };
28
+ import LanguageFilterSelect from "@/app/cms/components/LanguageFilterSelect";
29
+ import DeletePostButtonClient from "./components/DeletePostButtonClient"; // Import the new client component
30
+
31
+ async function getPostsWithDetails(filterLanguageId?: number): Promise<{ post: Post; languageCode: string }[]> {
32
+ const supabase = createClient();
33
+ const languages = await getActiveLanguagesServerSide();
34
+ const langMap = new Map(languages.map(l => [l.id, l.code]));
35
+
36
+ let query = supabase
37
+ .from("posts")
38
+ .select("*, languages!inner(code), media ( object_key )")
39
+ .order("created_at", { ascending: false });
40
+
41
+ if (filterLanguageId) {
42
+ query = query.eq("language_id", filterLanguageId);
43
+ }
44
+
45
+ const { data: postsData, error } = await query;
46
+
47
+ if (error) {
48
+ console.error("Error fetching posts:", error);
49
+ return [];
50
+ }
51
+ if (!postsData) return [];
52
+
53
+ return postsData.map(p => {
54
+ const langInfo = p.languages as unknown as { code: string } | null;
55
+ return {
56
+ post: { ...p, feature_image_url: p.media?.object_key ? `${process.env.NEXT_PUBLIC_R2_BASE_URL}/${p.media.object_key}` : null } as Post,
57
+ languageCode: langInfo?.code?.toUpperCase() || langMap.get(p.language_id)?.toUpperCase() || 'N/A',
58
+ };
59
+ });
60
+ }
61
+
62
+ interface CmsPostsListPageProps {
63
+ searchParams?: Promise<{
64
+ lang?: string;
65
+ success?: string;
66
+ }>;
67
+ }
68
+
69
+ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
70
+ const searchParams = await props.searchParams;
71
+ const allLanguages = await getActiveLanguagesServerSide();
72
+ const selectedLangId = searchParams?.lang ? parseInt(searchParams.lang, 10) : undefined;
73
+ const isValidLangId = selectedLangId ? allLanguages.some(l => l.id === selectedLangId) : true;
74
+ const filterLangId = isValidLangId ? selectedLangId : undefined;
75
+
76
+ const postsWithDetails = await getPostsWithDetails(filterLangId);
77
+ const successMessage = searchParams?.success;
78
+
79
+ return (
80
+ <div className="w-full">
81
+ <div className="flex justify-between items-center mb-6 flex-wrap gap-4">
82
+ <h1 className="text-2xl font-semibold">Manage Posts</h1>
83
+ <div className="flex items-center gap-3">
84
+ <LanguageFilterSelect
85
+ allLanguages={allLanguages}
86
+ currentFilterLangId={filterLangId}
87
+ basePath="/cms/posts"
88
+ />
89
+ <Button variant="default" asChild>
90
+ <Link href="/cms/posts/new">
91
+ <PlusCircle className="mr-2 h-4 w-4" /> Create New Post
92
+ </Link>
93
+ </Button>
94
+ </div>
95
+ </div>
96
+
97
+ {successMessage && (
98
+ <div className="mb-4 p-3 rounded-md text-sm bg-green-100 text-green-700 border border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700">
99
+ {decodeURIComponent(successMessage)}
100
+ </div>
101
+ )}
102
+
103
+ {postsWithDetails.length === 0 ? (
104
+ <div className="text-center py-10 border rounded-lg dark:border-slate-700">
105
+ <PenTool className="mx-auto h-12 w-12 text-muted-foreground" />
106
+ <h3 className="mt-2 text-sm font-medium text-foreground">
107
+ {filterLangId ? "No posts found for the selected language." : "No posts found."}
108
+ </h3>
109
+ <p className="mt-1 text-sm text-muted-foreground">
110
+ Get started by creating a new post.
111
+ </p>
112
+ <div className="mt-6">
113
+ <Button asChild>
114
+ <Link href="/cms/posts/new">
115
+ <PlusCircle className="mr-2 h-4 w-4" /> Create Post
116
+ </Link>
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ ) : (
121
+ <div className="rounded-lg border overflow-hidden dark:border-slate-700">
122
+ <Table>
123
+ <TableHeader>
124
+ <TableRow className="dark:border-slate-700">
125
+ <TableHead className="w-[300px] sm:w-[400px]">Title</TableHead>
126
+ <TableHead>Status</TableHead>
127
+ <TableHead>Language</TableHead>
128
+ <TableHead className="hidden md:table-cell">Slug</TableHead>
129
+ <TableHead className="hidden lg:table-cell">Published At</TableHead>
130
+ <TableHead className="text-right w-[80px]">Actions</TableHead>
131
+ </TableRow>
132
+ </TableHeader>
133
+ <TableBody>
134
+ {postsWithDetails.map(({ post, languageCode }) => (
135
+ <TableRow key={post.id} className="dark:border-slate-700">
136
+ <TableCell className="font-medium">
137
+ <Link
138
+ href={`/cms/posts/${post.id}/edit`}
139
+ className="flex items-center cursor-pointer"
140
+ >
141
+ <Edit3 className="mr-2 h-4 w-4" />
142
+ {post.title}
143
+ </Link>
144
+ </TableCell>
145
+ <TableCell>
146
+ <Badge
147
+ variant={
148
+ post.status === "published" ? "default" :
149
+ post.status === "draft" ? "secondary" : "destructive"
150
+ }
151
+ className={
152
+ post.status === "published" ? "bg-green-100 text-green-700 dark:bg-green-700/30 dark:text-green-300 dark:border-green-700/50" :
153
+ post.status === "draft" ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700/30 dark:text-yellow-300 dark:border-yellow-700/50" :
154
+ "bg-slate-100 text-slate-700 dark:bg-slate-700/30 dark:text-slate-300 dark:border-slate-600"
155
+ }
156
+ >
157
+ {post.status.charAt(0).toUpperCase() + post.status.slice(1)}
158
+ </Badge>
159
+ </TableCell>
160
+ <TableCell><Badge variant="outline" className="dark:border-slate-600">{languageCode}</Badge></TableCell>
161
+ <TableCell className="text-muted-foreground text-xs hidden md:table-cell">/blog/{post.slug}</TableCell>
162
+ <TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
163
+ {post.published_at ? new Date(post.published_at).toLocaleDateString() : "Not yet"}
164
+ </TableCell>
165
+ <TableCell className="text-right">
166
+ <DropdownMenu>
167
+ <DropdownMenuTrigger asChild>
168
+ <Button variant="ghost" size="icon">
169
+ <MoreHorizontal className="h-4 w-4" />
170
+ <span className="sr-only">Post actions for {post.title}</span>
171
+ </Button>
172
+ </DropdownMenuTrigger>
173
+ <DropdownMenuContent align="end">
174
+ <DropdownMenuItem asChild>
175
+ <Link href={`/cms/posts/${post.id}/edit`} className="flex items-center cursor-pointer">
176
+ <Edit3 className="mr-2 h-4 w-4" /> Edit
177
+ </Link>
178
+ </DropdownMenuItem>
179
+ <DropdownMenuSeparator />
180
+ <DeletePostButtonClient postId={post.id} />
181
+ </DropdownMenuContent>
182
+ </DropdownMenu>
183
+ </TableCell>
184
+ </TableRow>
185
+ ))}
186
+ </TableBody>
187
+ </Table>
188
+ </div>
189
+ )}
190
+ </div>
191
+ );
192
+ }