/** * Post Editor (Advanced) * Phase 7: Advanced Site Management * Full-featured editing: SEO, metadata, tags, categories, HTML content */ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { SaveIcon, XIcon, EyeIcon, FileTextIcon, SettingsIcon, TagIcon, CheckCircleIcon, XCircleIcon, AlertCircleIcon } from 'lucide-react'; import PageMeta from '../../components/common/PageMeta'; import { Card } from '../../components/ui/card'; import Button from '../../components/ui/button/Button'; import Label from '../../components/form/Label'; import TextArea from '../../components/form/input/TextArea'; import SelectDropdown from '../../components/form/SelectDropdown'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI, fetchContentValidation, validateContent, ContentValidationResult } from '../../services/api'; interface Content { id?: number; title: string; html_content?: string; content?: string; meta_title?: string; meta_description?: string; primary_keyword?: string; secondary_keywords?: string[]; tags?: string[]; categories?: string[]; content_type: string; status: string; site: number; sector: number; word_count?: number; metadata?: Record; // Stage 3: Metadata fields entity_type?: string | null; cluster_name?: string | null; cluster_id?: number | null; taxonomy_name?: string | null; taxonomy_id?: number | null; cluster_role?: string | null; } export default function PostEditor() { const { id: siteId, postId } = useParams<{ id: string; postId?: string }>(); const navigate = useNavigate(); const toast = useToast(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [activeTab, setActiveTab] = useState<'content' | 'seo' | 'metadata' | 'validation'>('content'); const [validationResult, setValidationResult] = useState(null); const [validating, setValidating] = useState(false); const [content, setContent] = useState({ title: '', html_content: '', content: '', meta_title: '', meta_description: '', primary_keyword: '', secondary_keywords: [], tags: [], categories: [], content_type: 'article', status: 'draft', site: Number(siteId), sector: 0, // Will be set from site }); const [tagInput, setTagInput] = useState(''); const [categoryInput, setCategoryInput] = useState(''); useEffect(() => { if (siteId) { loadSite(); if (postId && postId !== 'new') { loadPost(); loadValidation(); } else { setLoading(false); } } }, [siteId, postId]); const loadValidation = async () => { if (!postId || postId === 'new') return; try { const result = await fetchContentValidation(Number(postId)); setValidationResult(result); } catch (error: any) { console.error('Failed to load validation:', error); } }; const handleValidate = async () => { if (!content.id) { toast.error('Please save the content first before validating'); return; } try { setValidating(true); const result = await validateContent(content.id); await loadValidation(); if (result.is_valid) { toast.success('Content validation passed!'); } else { toast.warning(`Validation found ${result.errors.length} issue(s)`); } } catch (error: any) { toast.error(`Validation failed: ${error.message}`); } finally { setValidating(false); } }; const loadSite = async () => { try { const site = await fetchAPI(`/v1/auth/sites/${siteId}/`); if (site) { setContent((prev) => ({ ...prev, sector: site.sector || 0, })); } } catch (error: any) { console.error('Failed to load site:', error); } }; const loadPost = async () => { try { setLoading(true); const data = await fetchAPI(`/v1/writer/content/${postId}/`); if (data) { setContent({ id: data.id, title: data.title || '', html_content: data.html_content || '', content: data.html_content || data.content || '', meta_title: data.meta_title || '', meta_description: data.meta_description || '', primary_keyword: data.primary_keyword || '', secondary_keywords: Array.isArray(data.secondary_keywords) ? data.secondary_keywords : [], tags: Array.isArray(data.tags) ? data.tags : [], categories: Array.isArray(data.categories) ? data.categories : [], content_type: 'article', // Content model doesn't have content_type status: data.status || 'draft', site: data.site || Number(siteId), sector: data.sector || 0, word_count: data.word_count || 0, metadata: data.metadata || {}, }); } } catch (error: any) { toast.error(`Failed to load post: ${error.message}`); navigate(`/sites/${siteId}/content`); } finally { setLoading(false); } }; const handleSave = async () => { if (!content.title.trim()) { toast.error('Title is required'); return; } try { setSaving(true); const payload = { ...content, html_content: content.html_content || content.content, }; if (content.id) { // Update existing await fetchAPI(`/v1/writer/content/${content.id}/`, { method: 'PUT', body: JSON.stringify(payload), }); toast.success('Post updated successfully'); } else { // Create new - need to create a task first const taskData = await fetchAPI('/v1/writer/tasks/', { method: 'POST', body: JSON.stringify({ title: content.title, description: content.meta_description || '', keywords: content.primary_keyword || '', site_id: content.site, sector_id: content.sector, content_type: 'article', content_structure: 'blog_post', status: 'completed', }), }); if (taskData?.id) { const result = await fetchAPI('/v1/writer/content/', { method: 'POST', body: JSON.stringify({ ...payload, task_id: taskData.id, }), }); toast.success('Post created successfully'); if (result?.id) { navigate(`/sites/${siteId}/posts/${result.id}/edit`); } } } } catch (error: any) { toast.error(`Failed to save post: ${error.message}`); } finally { setSaving(false); } }; const handleAddTag = () => { if (tagInput.trim() && !content.tags?.includes(tagInput.trim())) { setContent({ ...content, tags: [...(content.tags || []), tagInput.trim()], }); setTagInput(''); } }; const handleRemoveTag = (tag: string) => { setContent({ ...content, tags: content.tags?.filter((t) => t !== tag) || [], }); }; const handleAddCategory = () => { if (categoryInput.trim() && !content.categories?.includes(categoryInput.trim())) { setContent({ ...content, categories: [...(content.categories || []), categoryInput.trim()], }); setCategoryInput(''); } }; const handleRemoveCategory = (category: string) => { setContent({ ...content, categories: content.categories?.filter((c) => c !== category) || [], }); }; const CONTENT_TYPES = [ { value: 'article', label: 'Article' }, { value: 'blog_post', label: 'Blog Post' }, { value: 'page', label: 'Page' }, { value: 'product', label: 'Product' }, ]; const STATUS_OPTIONS = [ { value: 'draft', label: 'Draft' }, { value: 'review', label: 'Review' }, { value: 'publish', label: 'Published' }, ]; if (loading) { return (
Loading post...
); } return (
{/* Main Content Area */}

{content.id ? 'Edit Post' : 'New Post'}

{content.id ? 'Edit your post content' : 'Create a new post'}

{/* Tabs */}
{content.id && ( )}
{/* Content Tab */} {activeTab === 'content' && (
setContent({ ...content, title: e.target.value })} placeholder="Enter post title" className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white" />
setContent({ ...content, content_type: value })} />
setContent({ ...content, status: value })} />