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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { Loader2, Trash2, ExternalLink, RefreshCw, Plus, X, Send, } from "lucide-react"; import { aethexToast } from "@/lib/aethex-toast"; const API_BASE = import.meta.env.VITE_API_BASE || ""; 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 [deleteConfirm, setDeleteConfirm] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [filterCategory, setFilterCategory] = useState(""); const [activeTab, setActiveTab] = useState("manage"); const [isPublishing, setIsPublishing] = useState(false); // Create post state const [title, setTitle] = useState(""); const [excerpt, setExcerpt] = useState(""); const [html, setHtml] = useState(""); const [slug, setSlug] = useState(""); const [featureImage, setFeatureImage] = useState(""); const [tags, setTags] = useState([]); const [tagInput, setTagInput] = useState(""); const [metaTitle, setMetaTitle] = useState(""); const [metaDescription, setMetaDescription] = useState(""); const loadBlogPosts = useCallback(async () => { setLoading(true); try { const res = await fetch(`${API_BASE}/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_BASE}/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 autoSlug = slug || title .toLowerCase() .replace(/[^\w\s-]/g, "") .trim() .replace(/\s+/g, "-") .replace(/-+/g, "-"); const addTag = () => { if (tagInput.trim() && !tags.includes(tagInput.trim())) { setTags([...tags, tagInput.trim()]); setTagInput(""); } }; const removeTag = (tag: string) => { setTags(tags.filter((t) => t !== tag)); }; const handlePublish = async () => { if (!title.trim() || !html.trim()) { aethexToast.error({ title: "Missing required fields", description: "Title and body are required", }); return; } setIsPublishing(true); try { const response = await fetch(`${API_BASE}/api/blog/publish`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, excerpt: excerpt || undefined, html, slug: autoSlug, feature_image: featureImage || undefined, tags, meta_title: metaTitle || title, meta_description: metaDescription || excerpt, status: "published", }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || "Failed to publish post"); } aethexToast.success({ title: "Post published!", description: "Successfully published to Ghost", }); // Reset form setTitle(""); setExcerpt(""); setHtml(""); setSlug(""); setFeatureImage(""); setTags([]); setMetaTitle(""); setMetaDescription(""); setActiveTab("manage"); loadBlogPosts(); } catch (error: any) { aethexToast.error({ title: "Failed to publish", description: error.message || "Unknown error", }); } finally { setIsPublishing(false); } }; 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 (
Manage Posts Create New {/* Manage Posts Tab */}
Blog Posts {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"}

) : (
Title Author Category Date Actions {filteredPosts.map((post) => (

{post.title}

{post.slug}

{post.author || "—"} {post.category ? ( {post.category} ) : ( )} {formatDate(post.published_at || post.date)}
))}
)}
{/* Create Post Tab */} Create New Post Publish directly to Ghost.org immediately {/* Title */}
setTitle(e.target.value)} placeholder="Post title" className="border-border/50" />
{/* Slug */}
setSlug(e.target.value)} placeholder="Leave blank to auto-generate" className="border-border/50" /> {!slug && title && (

Auto-slug:{" "} {autoSlug}

)}
{/* Excerpt */}