Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useState, useEffect } from 'react'; | |
| import { useRouter } from 'next/navigation'; | |
| import { useSession } from "next-auth/react"; | |
| import { useEditor, EditorContent } from '@tiptap/react'; | |
| import StarterKit from '@tiptap/starter-kit'; | |
| import LinkExtension from '@tiptap/extension-link'; | |
| import ImageExtension from '@tiptap/extension-image'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { Label } from '@/components/ui/label'; | |
| import { Textarea } from '@/components/ui/textarea'; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; | |
| import { Card, CardContent } from '@/components/ui/card'; | |
| import { Bold, Italic, List, ListOrdered, Link as LinkIcon, Image as ImageIcon, Loader2 } from 'lucide-react'; | |
| const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api'; | |
| interface BlogFormProps { | |
| initialData?: any; | |
| isEditing?: boolean; | |
| } | |
| export default function BlogForm({ initialData = null, isEditing = false }: BlogFormProps) { | |
| const router = useRouter(); | |
| const { data: session } = useSession(); | |
| const [isLoading, setIsLoading] = useState(false); | |
| // Form State | |
| const [title, setTitle] = useState(initialData?.title || ''); | |
| const [slug, setSlug] = useState(initialData?.slug || ''); | |
| const [excerpt, setExcerpt] = useState(initialData?.excerpt || ''); | |
| const [featuredImage, setFeaturedImage] = useState(initialData?.featuredImage || ''); | |
| const [status, setStatus] = useState(initialData?.status || 'DRAFT'); | |
| const [publishedAt, setPublishedAt] = useState(initialData?.publishedAt ? new Date(initialData.publishedAt).toISOString().slice(0, 16) : ''); | |
| const [tags, setTags] = useState(initialData?.tags ? JSON.parse(initialData.tags).join(', ') : ''); | |
| const editor = useEditor({ | |
| extensions: [ | |
| StarterKit, | |
| LinkExtension.configure({ openOnClick: false }), | |
| ImageExtension.configure({ inline: true }), | |
| ], | |
| content: initialData?.content || '<p>Start writing your post here...</p>', | |
| immediatelyRender: false, | |
| editorProps: { | |
| attributes: { | |
| class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[400px] p-4 border rounded-md max-w-none', | |
| }, | |
| }, | |
| }); | |
| const setLink = () => { | |
| const previousUrl = editor?.getAttributes('link').href; | |
| const url = window.prompt('URL', previousUrl); | |
| if (url === null) return; | |
| if (url === '') { | |
| editor?.chain().focus().extendMarkRange('link').unsetLink().run(); | |
| return; | |
| } | |
| editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); | |
| }; | |
| const addImage = () => { | |
| const url = window.prompt('Image URL'); | |
| if (url) { | |
| editor?.chain().focus().setImage({ src: url }).run(); | |
| } | |
| }; | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setIsLoading(true); | |
| const content = editor?.getHTML(); | |
| const tagsArray = tags.split(',').map((t: string) => t.trim()).filter(Boolean); | |
| const payload = { | |
| title, | |
| slug: slug || undefined, | |
| content, | |
| excerpt, | |
| featuredImage, | |
| status, | |
| publishedAt: publishedAt ? new Date(publishedAt).toISOString() : null, | |
| tags: JSON.stringify(tagsArray) | |
| }; | |
| try { | |
| const url = isEditing && initialData?.id | |
| ? `${API_URL}/admin/blog/${initialData.id}` | |
| : `${API_URL}/admin/blog`; | |
| const method = isEditing ? 'PUT' : 'POST'; | |
| const res = await fetch(url, { | |
| method, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${session?.user?.id}` | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (res.ok) { | |
| router.push('/admin/blog'); | |
| router.refresh(); | |
| } else { | |
| const data = await res.json(); | |
| alert(`Error: ${data.error || 'Failed to save post'}`); | |
| } | |
| } catch (error) { | |
| console.error("Save error:", error); | |
| alert("An unexpected error occurred while saving."); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit} className="space-y-8"> | |
| <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> | |
| <h2 className="text-3xl font-bold tracking-tight"> | |
| {isEditing ? 'Edit Article' : 'New Article'} | |
| </h2> | |
| <div className="flex gap-2 w-full md:w-auto"> | |
| <Button variant="outline" type="button" onClick={() => router.push('/admin/blog')}> | |
| Cancel | |
| </Button> | |
| <Button type="submit" disabled={isLoading || !title || !editor?.getHTML()}> | |
| {isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} | |
| {isEditing ? 'Save Changes' : 'Create Post'} | |
| </Button> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| {/* Main Content Area */} | |
| <div className="lg:col-span-2 space-y-6"> | |
| <Card> | |
| <CardContent className="p-6 space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="title">Post Title *</Label> | |
| <Input | |
| id="title" | |
| value={title} | |
| onChange={(e) => setTitle(e.target.value)} | |
| placeholder="Enter an engaging title..." | |
| required | |
| className="text-lg font-medium" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>Content *</Label> | |
| {editor && ( | |
| <div className="border rounded-md overflow-hidden bg-background"> | |
| {/* TipTap Toolbar */} | |
| <div className="flex flex-wrap items-center gap-1 p-1 border-b bg-muted/30"> | |
| <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive('bold') ? 'bg-muted' : ''}> | |
| <Bold className="h-4 w-4" /> | |
| </Button> | |
| <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive('italic') ? 'bg-muted' : ''}> | |
| <Italic className="h-4 w-4" /> | |
| </Button> | |
| <div className="w-px h-4 bg-border mx-1" /> | |
| <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={editor.isActive('heading', { level: 2 }) ? 'bg-muted' : ''}> | |
| H2 | |
| </Button> | |
| <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} className={editor.isActive('heading', { level: 3 }) ? 'bg-muted' : ''}> | |
| H3 | |
| </Button> | |
| <div className="w-px h-4 bg-border mx-1" /> | |
| <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleBulletList().run()} className={editor.isActive('bulletList') ? 'bg-muted' : ''}> | |
| <List className="h-4 w-4" /> | |
| </Button> | |
| <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleOrderedList().run()} className={editor.isActive('orderedList') ? 'bg-muted' : ''}> | |
| <ListOrdered className="h-4 w-4" /> | |
| </Button> | |
| <div className="w-px h-4 bg-border mx-1" /> | |
| <Button type="button" variant="ghost" size="sm" onClick={setLink} className={editor.isActive('link') ? 'bg-muted' : ''}> | |
| <LinkIcon className="h-4 w-4" /> | |
| </Button> | |
| <Button type="button" variant="ghost" size="sm" onClick={addImage}> | |
| <ImageIcon className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| {/* TipTap Editor */} | |
| <EditorContent editor={editor} /> | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="excerpt">Excerpt / Summary</Label> | |
| <Textarea | |
| id="excerpt" | |
| value={excerpt} | |
| onChange={(e) => setExcerpt(e.target.value)} | |
| placeholder="Brief summary for search engines and social sharing..." | |
| rows={3} | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Sidebar Metrics & Settings */} | |
| <div className="space-y-6"> | |
| <Card> | |
| <CardContent className="p-6 space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="status">Publishing Status</Label> | |
| <Select value={status} onValueChange={setStatus}> | |
| <SelectTrigger> | |
| <SelectValue placeholder="Select status" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="DRAFT">Draft</SelectItem> | |
| <SelectItem value="PUBLISHED">Published</SelectItem> | |
| <SelectItem value="SCHEDULED">Scheduled</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="publishedAt">Publish Date</Label> | |
| <Input | |
| id="publishedAt" | |
| type="datetime-local" | |
| value={publishedAt} | |
| onChange={(e) => setPublishedAt(e.target.value)} | |
| disabled={status === 'DRAFT'} | |
| /> | |
| {status === 'DRAFT' && <p className="text-xs text-muted-foreground">Date is disabled while in Draft status.</p>} | |
| </div> | |
| <div className="space-y-2 pt-4 border-t"> | |
| <Label htmlFor="slug">Custom URL Slug (Optional)</Label> | |
| <Input | |
| id="slug" | |
| value={slug} | |
| onChange={(e) => setSlug(e.target.value)} | |
| placeholder="e.g. my-awesome-post" | |
| /> | |
| <p className="text-xs text-muted-foreground">Leave blank to auto-generate from title.</p> | |
| </div> | |
| <div className="space-y-2 pt-4 border-t"> | |
| <Label htmlFor="featuredImage">Featured Image URL</Label> | |
| <Input | |
| id="featuredImage" | |
| value={featuredImage} | |
| onChange={(e) => setFeaturedImage(e.target.value)} | |
| placeholder="https://..." | |
| /> | |
| {featuredImage && ( | |
| <div className="mt-2 text-xs text-muted-foreground overflow-hidden rounded-md h-32 bg-muted relative"> | |
| {/* eslint-disable-next-line @next/next/no-img-element */} | |
| <img src={featuredImage} alt="Featured preview" className="object-cover w-full h-full" /> | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-2 pt-4 border-t"> | |
| <Label htmlFor="tags">Tags (Comma separated)</Label> | |
| <Input | |
| id="tags" | |
| value={tags} | |
| onChange={(e) => setTags(e.target.value)} | |
| placeholder="React, Design, Tech" | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| </form> | |
| ); | |
| } | |