From c087669079335c751ac608468063891f9a988f38 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 11 Nov 2025 03:21:58 +0000 Subject: [PATCH] Create comprehensive blog management component for admin panel cgen-ec91e95ed662404da8a71a57c9a1b5cc --- client/components/admin/AdminBlogManager.tsx | 314 +++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 client/components/admin/AdminBlogManager.tsx diff --git a/client/components/admin/AdminBlogManager.tsx b/client/components/admin/AdminBlogManager.tsx new file mode 100644 index 00000000..89c70ab8 --- /dev/null +++ b/client/components/admin/AdminBlogManager.tsx @@ -0,0 +1,314 @@ +import { useCallback, useEffect, useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Loader2, Trash2, ExternalLink, RefreshCw } from "lucide-react"; +import { aethexToast } from "@/lib/aethex-toast"; + +interface BlogPost { + id?: string; + slug: string; + title: string; + excerpt?: string | null; + author?: string | null; + date?: string | null; + category?: string | null; + image?: string | null; + published_at?: string | null; + body_html?: string | null; +} + +export default function AdminBlogManager() { + const [blogPosts, setBlogPosts] = useState([]); + const [loading, setLoading] = useState(false); + const [deleting, setDeleting] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [filterCategory, setFilterCategory] = useState(""); + + const loadBlogPosts = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/blog?limit=100"); + if (res.ok) { + const data = await res.json(); + if (Array.isArray(data)) { + setBlogPosts(data); + aethexToast.success({ + title: "Blog posts loaded", + description: `Loaded ${data.length} blog posts`, + }); + } + } else { + const errorText = await res.text(); + console.error("Failed to load blog posts:", errorText); + aethexToast.error({ + title: "Failed to load blog posts", + description: res.statusText || "Unknown error", + }); + } + } catch (error) { + console.error("Error loading blog posts:", error); + aethexToast.error({ + title: "Error loading blog posts", + description: String(error), + }); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadBlogPosts(); + }, [loadBlogPosts]); + + const handleDeleteBlogPost = useCallback( + async (slug: string) => { + setDeleting(slug); + try { + const res = await fetch(`/api/blog/${slug}`, { method: "DELETE" }); + if (res.ok) { + setBlogPosts((posts) => posts.filter((p) => p.slug !== slug)); + aethexToast.success({ + title: "Blog post deleted", + description: `Post "${slug}" has been removed`, + }); + } else { + aethexToast.error({ + title: "Failed to delete blog post", + description: res.statusText || "Unknown error", + }); + } + } catch (error) { + console.error("Error deleting blog post:", error); + aethexToast.error({ + title: "Error deleting blog post", + description: String(error), + }); + } finally { + setDeleting(null); + } + }, + [], + ); + + const filteredPosts = blogPosts.filter((post) => { + const matchesSearch = + post.title.toLowerCase().includes(searchQuery.toLowerCase()) || + post.slug.toLowerCase().includes(searchQuery.toLowerCase()) || + (post.author && post.author.toLowerCase().includes(searchQuery.toLowerCase())); + const matchesCategory = !filterCategory || post.category === filterCategory; + return matchesSearch && matchesCategory; + }); + + const categories = Array.from(new Set(blogPosts.map((p) => p.category).filter(Boolean))); + + const formatDate = (dateStr?: string | null) => { + if (!dateStr) return "—"; + try { + return new Date(dateStr).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } catch { + return dateStr; + } + }; + + return ( +
+ + +
+
+ Blog Management + + {blogPosts.length} published {blogPosts.length === 1 ? "post" : "posts"} + +
+ +
+
+ +
+
+ + setSearchQuery(e.target.value)} + className="h-8" + /> +
+
+ + +
+
+ + {filteredPosts.length === 0 ? ( +
+

+ {blogPosts.length === 0 ? "No blog posts found" : "No matching blog posts"} +

+ {blogPosts.length === 0 && ( + + )} +
+ ) : ( +
+ + + + Title + Author + Category + Date + Actions + + + + {filteredPosts.map((post) => ( + + +
+

{post.title}

+

{post.slug}

+
+
+ + {post.author || "—"} + + + {post.category ? ( + + {post.category} + + ) : ( + + )} + + + {formatDate(post.published_at || post.date)} + + +
+ + + + {deleting === post.slug && ( + + + Delete blog post? + + Are you sure you want to delete "{post.title}"? This action cannot be + undone. + + +
+ setDeleting(null)} + > + Cancel + + { + handleDeleteBlogPost(post.slug); + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Delete + +
+
+ )} +
+
+
+
+ ))} +
+
+
+ )} +
+
+
+ ); +}