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,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 'Published'.</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
|
+
}
|