import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/lib/supabaseClient'; import { useAuth } from '@/hooks/useAuth'; import { useUserRole } from '@/hooks/useUserRole'; import { AdminLayout } from '@/components/layout/AdminLayout'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; import { UppyPhotoUploadLazy } from '@/components/upload/UppyPhotoUploadLazy'; import { MarkdownEditorLazy } from '@/components/admin/MarkdownEditorLazy'; import { generateSlugFromName } from '@/lib/slugUtils'; import { extractCloudflareImageId } from '@/lib/cloudflareImageUtils'; import { Edit, Trash2, Eye, Plus } from 'lucide-react'; import { toast } from 'sonner'; import { formatDistanceToNow } from 'date-fns'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; interface BlogPost { id: string; slug: string; title: string; content: string; featured_image_id?: string; featured_image_url?: string; status: 'draft' | 'published'; published_at?: string; view_count: number; created_at: string; } export default function AdminBlog() { useDocumentTitle('Blog Management - Admin'); const { user } = useAuth(); const { isAdmin, loading } = useUserRole(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [activeTab, setActiveTab] = useState('posts'); const [editingPost, setEditingPost] = useState(null); const [title, setTitle] = useState(''); const [slug, setSlug] = useState(''); const [content, setContent] = useState(''); const [featuredImageId, setFeaturedImageId] = useState(''); const [featuredImageUrl, setFeaturedImageUrl] = useState(''); // All mutations must be called before conditional returns const saveMutation = useMutation({ mutationFn: async ({ isDraft }: { isDraft: boolean }) => { const postData = { title, slug, content, featured_image_id: featuredImageId || null, featured_image_url: featuredImageUrl || null, author_id: user!.id, status: isDraft ? 'draft' : 'published', published_at: isDraft ? null : new Date().toISOString(), }; if (editingPost) { const { error } = await supabase .from('blog_posts') .update(postData) .eq('id', editingPost.id); if (error) throw error; } else { const { error } = await supabase .from('blog_posts') .insert(postData); if (error) throw error; } }, onSuccess: (_, { isDraft }) => { queryClient.invalidateQueries({ queryKey: ['admin-blog-posts'] }); toast.success(isDraft ? 'Draft saved' : 'Post published'); resetForm(); setActiveTab('posts'); }, onError: (error) => { toast.error('Failed to save post: ' + error.message); }, }); const deleteMutation = useMutation({ mutationFn: async (id: string) => { const { error } = await supabase .from('blog_posts') .delete() .eq('id', id); if (error) throw error; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-blog-posts'] }); toast.success('Post deleted'); }, onError: (error) => { toast.error('Failed to delete post: ' + error.message); }, }); const { data: posts, isLoading } = useQuery({ queryKey: ['admin-blog-posts'], queryFn: async () => { const { data, error } = await supabase .from('blog_posts') .select('*') .order('created_at', { ascending: false }); if (error) throw error; return data as BlogPost[]; }, }); // useEffect must be called before conditional returns useEffect(() => { if (!loading && !isAdmin()) { navigate('/'); } }, [loading, isAdmin, navigate]); // Show loading state while checking permissions if (loading) { return (

Loading...

); } // Don't render if not admin if (!loading && !isAdmin()) { return null; } const resetForm = () => { setTitle(''); setSlug(''); setContent(''); setFeaturedImageId(''); setFeaturedImageUrl(''); setEditingPost(null); }; const loadPostForEditing = (post: BlogPost) => { setEditingPost(post); setTitle(post.title); setSlug(post.slug); setContent(post.content); setFeaturedImageId(post.featured_image_id || ''); setFeaturedImageUrl(post.featured_image_url || ''); setActiveTab('editor'); }; const handleTitleChange = (newTitle: string) => { setTitle(newTitle); if (!editingPost) { setSlug(generateSlugFromName(newTitle)); } }; const handleImageUpload = (imageId: string, imageUrl: string) => { setFeaturedImageId(imageId); setFeaturedImageUrl(imageUrl); }; return (

Blog Management

Create and manage blog posts

Posts {editingPost ? 'Edit Post' : 'New Post'}

{posts?.length || 0} total posts

Title Status Published Views Actions {isLoading ? ( Loading... ) : posts?.length === 0 ? ( No posts yet. Create your first post! ) : ( posts?.map((post) => ( {post.title} {post.status} {post.published_at ? formatDistanceToNow(new Date(post.published_at), { addSuffix: true }) : '-' } {post.view_count} )) )}
handleTitleChange(e.target.value)} placeholder="Enter post title..." />
setSlug(e.target.value)} placeholder="post-url-slug" />

URL: /blog/{slug || 'post-url-slug'}

{ if (urls.length > 0) { const url = urls[0]; const imageId = extractCloudflareImageId(url); if (imageId) { handleImageUpload(imageId, url); toast.success('Image uploaded'); } } }} maxFiles={1} /> {featuredImageUrl && ( Preview )}
{ if (editingPost) { await supabase .from('blog_posts') .update({ content: value, updated_at: new Date().toISOString() }) .eq('id', editingPost.id); } }} autoSave={!!editingPost} height={600} placeholder="Write your blog post in markdown..." />
); }