feat: Complete Stage 2 frontend refactor
- Removed deprecated fields from Content and Task models, including entity_type, sync_status, and cluster_role. - Updated Content model to include new fields: content_type, content_structure, taxonomy_terms, source, external_id, and cluster_id. - Refactored Writer module components (Content, ContentView, Dashboard, Tasks) to align with new schema. - Enhanced Dashboard metrics and removed unused filters. - Implemented ClusterDetail page to display cluster information and associated content. - Updated API service interfaces to reflect changes in data structure. - Adjusted sorting and filtering logic across various components to accommodate new field names and types. - Improved user experience by providing loading states and error handling in data fetching.
This commit is contained in:
@@ -24,16 +24,14 @@ import {
|
||||
interface ContentItem {
|
||||
id: number;
|
||||
title: string;
|
||||
meta_title?: string;
|
||||
meta_description?: string;
|
||||
status: string;
|
||||
word_count: number;
|
||||
generated_at: string;
|
||||
updated_at: string;
|
||||
source: string;
|
||||
sync_status: string;
|
||||
task_id?: number;
|
||||
primary_keyword?: string;
|
||||
content_type?: string;
|
||||
content_structure?: string;
|
||||
cluster_name?: string;
|
||||
external_url?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export default function SiteContentManager() {
|
||||
@@ -45,7 +43,7 @@ export default function SiteContentManager() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [sourceFilter, setSourceFilter] = useState('');
|
||||
const [sortBy, setSortBy] = useState<'generated_at' | 'updated_at' | 'word_count' | 'title'>('generated_at');
|
||||
const [sortBy, setSortBy] = useState<'created_at' | 'updated_at' | 'title'>('created_at');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
@@ -111,8 +109,7 @@ export default function SiteContentManager() {
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: 'All Statuses' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'review', label: 'Review' },
|
||||
{ value: 'publish', label: 'Published' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
];
|
||||
|
||||
const SOURCE_OPTIONS = [
|
||||
@@ -195,11 +192,9 @@ export default function SiteContentManager() {
|
||||
}}
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
>
|
||||
<option value="generated_at-desc">Newest First</option>
|
||||
<option value="generated_at-asc">Oldest First</option>
|
||||
<option value="created_at-desc">Newest First</option>
|
||||
<option value="created_at-asc">Oldest First</option>
|
||||
<option value="updated_at-desc">Recently Updated</option>
|
||||
<option value="word_count-desc">Most Words</option>
|
||||
<option value="word_count-asc">Fewest Words</option>
|
||||
<option value="title-asc">Title A-Z</option>
|
||||
<option value="title-desc">Title Z-A</option>
|
||||
</select>
|
||||
@@ -231,22 +226,16 @@ export default function SiteContentManager() {
|
||||
>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{item.title || item.meta_title || `Content #${item.id}`}
|
||||
{item.title || `Content #${item.id}`}
|
||||
</h3>
|
||||
{item.meta_description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2 line-clamp-2">
|
||||
{item.meta_description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-500">
|
||||
<span className={`px-2 py-1 rounded ${item.status === 'publish' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : item.status === 'review' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}`}>
|
||||
<span className={`px-2 py-1 rounded ${item.status === 'published' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
<span>{item.word_count.toLocaleString()} words</span>
|
||||
{item.content_type && <span>{item.content_type}</span>}
|
||||
{item.content_structure && <span>{item.content_structure}</span>}
|
||||
<span>{item.source}</span>
|
||||
{item.primary_keyword && (
|
||||
<span>Keyword: {item.primary_keyword}</span>
|
||||
)}
|
||||
{item.cluster_name && <span>Cluster: {item.cluster_name}</span>}
|
||||
<span>
|
||||
{new Date(item.updated_at).toLocaleDateString()}
|
||||
</span>
|
||||
|
||||
@@ -18,27 +18,20 @@ import { fetchAPI, fetchContentValidation, validateContent, ContentValidationRes
|
||||
interface Content {
|
||||
id?: number;
|
||||
title: string;
|
||||
html_content?: string;
|
||||
content_html?: 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<string, any>;
|
||||
// Stage 3: Metadata fields
|
||||
entity_type?: string | null;
|
||||
cluster_name?: string | null;
|
||||
content_type: string; // post, page, product, service, category, tag
|
||||
content_structure?: string; // article, listicle, guide, comparison, product_page
|
||||
status: string; // draft, published
|
||||
site?: number;
|
||||
cluster_id?: number | null;
|
||||
taxonomy_name?: string | null;
|
||||
taxonomy_id?: number | null;
|
||||
cluster_role?: string | null;
|
||||
cluster_name?: string | null;
|
||||
taxonomy_terms?: Array<{ id: number; name: string; taxonomy: string }> | null;
|
||||
source?: string; // igny8, wordpress
|
||||
external_id?: string | null;
|
||||
external_url?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export default function PostEditor() {
|
||||
@@ -52,21 +45,15 @@ export default function PostEditor() {
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [content, setContent] = useState<Content>({
|
||||
title: '',
|
||||
html_content: '',
|
||||
content_html: '',
|
||||
content: '',
|
||||
meta_title: '',
|
||||
meta_description: '',
|
||||
primary_keyword: '',
|
||||
secondary_keywords: [],
|
||||
tags: [],
|
||||
categories: [],
|
||||
content_type: 'article',
|
||||
content_type: 'post',
|
||||
content_structure: 'article',
|
||||
status: 'draft',
|
||||
site: Number(siteId),
|
||||
sector: 0, // Will be set from site
|
||||
source: 'igny8',
|
||||
taxonomy_terms: [],
|
||||
});
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [categoryInput, setCategoryInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
@@ -134,20 +121,20 @@ export default function PostEditor() {
|
||||
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
|
||||
content_html: data.content_html || '',
|
||||
content: data.content_html || data.content || '',
|
||||
content_type: data.content_type || 'post',
|
||||
content_structure: data.content_structure || 'article',
|
||||
status: data.status || 'draft',
|
||||
site: data.site || Number(siteId),
|
||||
sector: data.sector || 0,
|
||||
word_count: data.word_count || 0,
|
||||
metadata: data.metadata || {},
|
||||
cluster_id: data.cluster_id || null,
|
||||
cluster_name: data.cluster_name || null,
|
||||
taxonomy_terms: Array.isArray(data.taxonomy_terms) ? data.taxonomy_terms : [],
|
||||
source: data.source || 'igny8',
|
||||
external_id: data.external_id || null,
|
||||
external_url: data.external_url || null,
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -167,8 +154,17 @@ export default function PostEditor() {
|
||||
try {
|
||||
setSaving(true);
|
||||
const payload = {
|
||||
...content,
|
||||
html_content: content.html_content || content.content,
|
||||
title: content.title,
|
||||
content_html: content.content_html || content.content,
|
||||
content_type: content.content_type,
|
||||
content_structure: content.content_structure,
|
||||
status: content.status,
|
||||
site: content.site,
|
||||
cluster_id: content.cluster_id,
|
||||
taxonomy_terms: content.taxonomy_terms,
|
||||
source: content.source || 'igny8',
|
||||
external_id: content.external_id,
|
||||
external_url: content.external_url,
|
||||
};
|
||||
|
||||
if (content.id) {
|
||||
@@ -179,33 +175,16 @@ export default function PostEditor() {
|
||||
});
|
||||
toast.success('Post updated successfully');
|
||||
} else {
|
||||
// Create new - need to create a task first
|
||||
const taskData = await fetchAPI('/v1/writer/tasks/', {
|
||||
// Create new
|
||||
const result = await fetchAPI('/v1/writer/content/', {
|
||||
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',
|
||||
...payload,
|
||||
}),
|
||||
});
|
||||
|
||||
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`);
|
||||
}
|
||||
toast.success('Post created successfully');
|
||||
if (result?.id) {
|
||||
navigate(`/sites/${siteId}/posts/${result.id}/edit`);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -215,51 +194,26 @@ export default function PostEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
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: 'post', label: 'Post' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'category', label: 'Category' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
];
|
||||
|
||||
const CONTENT_STRUCTURES = [
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'listicle', label: 'Listicle' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'comparison', label: 'Comparison' },
|
||||
{ value: 'product_page', label: 'Product Page' },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'review', label: 'Review' },
|
||||
{ value: 'publish', label: 'Published' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
|
||||
Reference in New Issue
Block a user