Files
igny8/frontend/src/pages/Writer/Content.tsx
IGNY8 VPS (Salman) e736697d6d text udpates ux
2025-12-25 11:51:44 +00:00

410 lines
14 KiB
TypeScript

/**
* Content Page - Built with TablePageTemplate
* Displays content from Content table with filters and pagination
*/
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContent,
Content as ContentType,
ContentFilters,
generateImagePrompts,
deleteContent,
bulkDeleteContent,
} from '../../services/api';
import { optimizerApi } from '../../api/optimizer.api';
import { useNavigate } from 'react-router-dom';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons';
import { createContentPageConfig } from '../../config/pages/content.config';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import ProgressModal from '../../components/common/ProgressModal';
import { useProgressModal } from '../../hooks/useProgressModal';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
import { WorkflowInsight } from '../../components/common/WorkflowInsights';
export default function Content() {
const toast = useToast();
const { activeSector } = useSectorStore();
const { pageSize } = usePageSizeStore();
// Data state
const [content, setContent] = useState<ContentType[]>([]);
const [loading, setLoading] = useState(true);
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('draft');
const [sourceFilter, setSourceFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
// Sorting state
const [sortBy, setSortBy] = useState<string>('created_at');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// Progress modal for AI functions
const progressModal = useProgressModal();
const hasReloadedRef = useRef(false);
// Calculate workflow insights
const workflowInsights: WorkflowInsight[] = useMemo(() => {
const insights: WorkflowInsight[] = [];
const draftCount = content.filter(c => c.status === 'draft').length;
const reviewCount = content.filter(c => c.status === 'review').length;
const publishedCount = content.filter(c => c.status === 'published').length;
const publishingRate = totalCount > 0 ? Math.round((publishedCount / totalCount) * 100) : 0;
if (totalCount === 0) {
insights.push({
type: 'info',
message: 'No content yet - Generate content from tasks to build your content library',
});
return insights;
}
// Draft vs Review status
if (draftCount > reviewCount * 3 && draftCount >= 5) {
insights.push({
type: 'warning',
message: `${draftCount} drafts waiting for review - Move content to review stage for quality assurance`,
});
} else if (draftCount > 0) {
insights.push({
type: 'info',
message: `${draftCount} drafts in progress - Review and refine before moving to publish stage`,
});
}
// Review queue status
if (reviewCount > 0) {
insights.push({
type: 'action',
message: `${reviewCount} pieces awaiting final review - Approve and publish when ready`,
});
}
// Publishing readiness
if (publishingRate >= 60 && publishedCount >= 10) {
insights.push({
type: 'success',
message: `Strong publishing rate (${publishingRate}%) - ${publishedCount} articles ready for WordPress sync`,
});
} else if (publishedCount > 0) {
insights.push({
type: 'success',
message: `${publishedCount} articles published (${publishingRate}%) - Continue moving content through the pipeline`,
});
}
return insights;
}, [content, totalCount]);
// Load content - wrapped in useCallback
const loadContent = useCallback(async () => {
setLoading(true);
setShowContent(false);
try {
const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at';
const filters: ContentFilters = {
...(searchTerm && { search: searchTerm }),
...(statusFilter && { status: statusFilter }),
...(sourceFilter && { source: sourceFilter }),
page: currentPage,
page_size: pageSize,
ordering,
};
const data = await fetchContent(filters);
setContent(data.results || []);
setTotalCount(data.count || 0);
setTotalPages(Math.ceil((data.count || 0) / pageSize));
setTimeout(() => {
setShowContent(true);
setLoading(false);
}, 100);
} catch (error: any) {
console.error('Error loading content:', error);
toast.error(`Failed to load content: ${error.message}`);
setShowContent(true);
setLoading(false);
}
}, [currentPage, statusFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize, toast]);
useEffect(() => {
loadContent();
}, [loadContent]);
// Listen for site and sector changes and refresh data
useEffect(() => {
const handleSiteChange = () => {
loadContent();
};
const handleSectorChange = () => {
loadContent();
};
window.addEventListener('siteChanged', handleSiteChange);
window.addEventListener('sectorChanged', handleSectorChange);
return () => {
window.removeEventListener('siteChanged', handleSiteChange);
window.removeEventListener('sectorChanged', handleSectorChange);
};
}, [loadContent]);
// Reset to page 1 when pageSize changes
useEffect(() => {
setCurrentPage(1);
}, [pageSize]);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadContent();
} else {
setCurrentPage(1);
}
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadContent]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
setSortBy(field || 'generated_at');
setSortDirection(direction);
setCurrentPage(1);
};
const navigate = useNavigate();
// Handle row click - navigate to content view
const handleRowClick = useCallback((row: ContentType) => {
navigate(`/writer/content/${row.id}`);
}, [navigate]);
// Create page config
const pageConfig = useMemo(() => {
return createContentPageConfig({
activeSector,
searchTerm,
setSearchTerm,
statusFilter,
setStatusFilter,
setCurrentPage,
onRowClick: handleRowClick,
});
}, [
activeSector,
searchTerm,
statusFilter,
handleRowClick,
]);
// Calculate header metrics
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ content, totalCount }),
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, content, totalCount]);
const handleRowAction = useCallback(async (action: string, row: ContentType) => {
if (action === 'view_on_wordpress') {
if (row.external_url) {
window.open(row.external_url, '_blank');
} else {
toast.warning('WordPress URL not available');
}
} else if (action === 'generate_image_prompts') {
try {
const result = await generateImagePrompts([row.id]);
if (result.success) {
if (result.task_id) {
// Open progress modal for async task
progressModal.openModal(
result.task_id,
'Smart Image Prompts',
'ai-generate-image-prompts-01-desktop'
);
} else {
// Synchronous completion
toast.success(`Image prompts generation task started. Task ID: ${result.task_id || 'N/A'}`);
loadContent(); // Reload to show new prompts
}
} else {
toast.error(result.error || 'Failed to generate image prompts');
}
} catch (error: any) {
toast.error(`Failed to generate prompts: ${error.message}`);
}
} else if (action === 'optimize') {
try {
const result = await optimizerApi.optimize(row.id, 'writer');
toast.success(`Content optimized! Score: ${result.scores_after.overall_score.toFixed(1)}`);
loadContent(); // Reload to show updated scores
} catch (error: any) {
toast.error(`Failed to optimize content: ${error.message}`);
}
} else if (action === 'send_to_optimizer') {
navigate(`/optimizer/content?contentId=${row.id}`);
}
}, [toast, progressModal, loadContent, navigate]);
const handleDelete = useCallback(async (id: number) => {
await deleteContent(id);
loadContent();
}, [loadContent]);
const handleBulkDelete = useCallback(async (ids: number[]) => {
const result = await bulkDeleteContent(ids);
loadContent();
return result;
}, [loadContent]);
// Writer navigation tabs
const writerTabs = [
{ label: 'Ready to Write', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Finished Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Article Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review Before Publishing', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
];
return (
<>
<PageHeader
title="Your Articles"
badge={{ icon: <FileIcon />, color: 'purple' }}
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
workflowInsights={workflowInsights}
/>
<TablePageTemplate
columns={pageConfig.columns}
data={content}
loading={loading}
showContent={showContent}
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
status: statusFilter,
source: sourceFilter,
}}
onFilterChange={(key: string, value: any) => {
if (key === 'search') {
setSearchTerm(value);
} else if (key === 'status') {
setStatusFilter(value);
setCurrentPage(1);
} else if (key === 'source') {
setSourceFilter(value);
setCurrentPage(1);
}
}}
pagination={{
currentPage,
totalPages,
totalCount,
onPageChange: setCurrentPage,
}}
sorting={{
sortBy,
sortDirection,
onSort: handleSort,
}}
selection={{
selectedIds,
onSelectionChange: setSelectedIds,
}}
headerMetrics={headerMetrics}
onRowAction={handleRowAction}
onDelete={handleDelete}
onBulkDelete={handleBulkDelete}
getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`}
/>
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
<ModuleMetricsFooter
metrics={[
{
title: 'Tasks',
value: content.length.toLocaleString(),
subtitle: 'generated from queue',
icon: <TaskIcon className="w-5 h-5" />,
accentColor: 'blue',
href: '/writer/tasks',
},
{
title: 'Draft',
value: content.filter(c => c.status === 'draft').length.toLocaleString(),
subtitle: 'needs editing',
icon: <FileIcon className="w-5 h-5" />,
accentColor: 'amber',
},
{
title: 'In Review',
value: content.filter(c => c.status === 'review').length.toLocaleString(),
subtitle: 'awaiting approval',
icon: <CheckCircleIcon className="w-5 h-5" />,
accentColor: 'blue',
href: '/writer/review',
},
{
title: 'Published',
value: content.filter(c => c.status === 'published').length.toLocaleString(),
subtitle: 'ready for sync',
icon: <CheckCircleIcon className="w-5 h-5" />,
accentColor: 'green',
href: '/writer/published',
},
]}
progress={{
label: 'Content Publishing Pipeline: Content moved from draft through review to published (Draft \u2192 Review \u2192 Published)',
value: totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0,
color: 'success',
}}
/>
{/* Progress Modal for AI Functions */}
<ProgressModal
isOpen={progressModal.isOpen}
title={progressModal.title}
percentage={progressModal.progress.percentage}
status={progressModal.progress.status}
message={progressModal.progress.message}
details={progressModal.progress.details}
taskId={progressModal.taskId || undefined}
functionId={progressModal.functionId}
onClose={() => {
const wasCompleted = progressModal.progress.status === 'completed';
progressModal.closeModal();
// Reload data after modal closes (if completed)
if (wasCompleted && !hasReloadedRef.current) {
hasReloadedRef.current = true;
loadContent();
setTimeout(() => {
hasReloadedRef.current = false;
}, 1000);
}
}}
/>
</>
);
}