diff --git a/app/admin/blog-posts/page.tsx b/app/admin/blog-posts/page.tsx new file mode 100644 index 00000000..8b02e467 --- /dev/null +++ b/app/admin/blog-posts/page.tsx @@ -0,0 +1,829 @@ +"use client" + +import { useState, useEffect, useCallback, useMemo } from "react" +import { createClient } from "@/lib/supabase/client" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Label } from "@/components/ui/label" +import { Checkbox } from "@/components/ui/checkbox" +import { AlertCircle, FileText, Search, MoreHorizontal, Edit, Star, Trash2, PlusCircle, Loader2, RefreshCw } from "lucide-react" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { categories, BlogPost } from "@/components/data/blog-posts" + +// types +interface BlogFormData { + title: string; + excerpt: string; + content: string; + author: string; + date: string; + readTime: string; + category: string; + tags: string; + featured: boolean; + image: string; + views: string; + likes: number; +} + +interface ApiError { + message: string; + details?: string; +} + +// utilities +const parseTags = (tags: string | string[]): string[] => { + if (Array.isArray(tags)) return tags + if (typeof tags === "string" && tags.trim()) { + return tags.split(",").map((t) => t.trim()).filter(Boolean) + } + return [] +} + +const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }) +} + +const getEmptyPost = (): BlogFormData => ({ + title: "", + excerpt: "", + content: "", + author: "", + date: new Date().toISOString().slice(0, 10), + readTime: "5 min", + category: categories[1] || "Frontend", + tags: "", + featured: false, + image: "", + views: "0", + likes: 0, +}) + +// custom hooks +const useSupabase = () => { + const supabase = useMemo(() => createClient(), []) + return supabase +} + +const useBlogPosts = () => { + const [blogPosts, setBlogPosts] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const supabase = useSupabase() + + const fetchPosts = useCallback(async () => { + try { + setIsLoading(true) + setError(null) + + const { data, error: fetchError } = await supabase + .from("blogs") + .select("*") + .order("date", { ascending: false }) + + if (fetchError) { + throw new Error(fetchError.message) + } + + if (data) { + setBlogPosts(data as BlogPost[]) + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to fetch blog posts" + setError({ message: errorMessage }) + setBlogPosts([]) + } finally { + setIsLoading(false) + } + }, [supabase]) + + useEffect(() => { + fetchPosts() + }, [fetchPosts]) + + return { blogPosts, isLoading, error, refetch: fetchPosts } +} + +// components +const CategoryBadge = ({ category }: { category: string }) => ( + + {category} + +) + +const FeaturedBadge = ({ featured }: { featured: boolean }) => ( + featured ? ( + + + Featured + + ) : ( + + Regular + + ) +) + +const BlogPostForm = ({ + formData, + onFormChange +}: { + formData: BlogFormData; + onFormChange: (data: Partial) => void; +}) => { + const handleInputChange = (field: keyof BlogFormData) => ( + e: React.ChangeEvent + ) => { + onFormChange({ [field]: e.target.value }) + } + + const handleSelectChange = (field: keyof BlogFormData) => (value: string) => { + onFormChange({ [field]: value }) + } + + const handleCheckboxChange = (checked: boolean) => { + onFormChange({ featured: checked }) + } + + return ( +
+
+ + +
+ +
+ + +
+ +
+ +