Files
igny8/frontend/src/pages/Writer/Published.tsx
IGNY8 VPS (Salman) 62fc47cfe8 UX: Update Writer module pages with user-friendly text
- Tasks: Changed 'Content Queue' to 'Writing Tasks'
- Content: Changed 'Content Drafts' to 'Your Articles'
- Review: Changed 'Content Review' to 'Review Queue'
- Published: Changed 'Published Content' to 'Published Articles'
- Images: Changed 'Content Images' to 'Article Images'
- Dashboard: Changed 'Writer Dashboard' to 'Content Creation Dashboard'
2025-12-25 09:01:18 +00:00

399 lines
13 KiB
TypeScript

/**
* Published Page - Built with TablePageTemplate
* Shows published/review content with WordPress publishing capabilities
*/
import { useState, useEffect, useMemo, useCallback } from 'react';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContent,
Content,
ContentListResponse,
ContentFilters,
fetchAPI,
fetchWordPressStatus,
deleteContent,
bulkDeleteContent,
} from '../../services/api';
import { useNavigate } from 'react-router-dom';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons';
import { createPublishedPageConfig } from '../../config/pages/published.config';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter';
export default function Published() {
const toast = useToast();
const navigate = useNavigate();
const { activeSector } = useSectorStore();
const { pageSize } = usePageSizeStore();
// Data state
const [content, setContent] = useState<Content[]>([]);
const [loading, setLoading] = useState(true);
// Filter state - default to published/review status
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('published'); // Default to published
const [publishStatusFilter, setPublishStatusFilter] = 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);
// Load content - filtered for published/review
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 }),
page: currentPage,
page_size: pageSize,
ordering,
};
const data: ContentListResponse = await fetchContent(filters);
// Client-side filter for WordPress publish status if needed
let filteredResults = data.results || [];
if (publishStatusFilter === 'published') {
filteredResults = filteredResults.filter(c => c.external_id);
} else if (publishStatusFilter === 'not_published') {
filteredResults = filteredResults.filter(c => !c.external_id);
}
// Fetch WordPress status for published content
const resultsWithWPStatus = await Promise.all(
filteredResults.map(async (content) => {
if (content.external_id) {
try {
const wpStatus = await fetchWordPressStatus(content.id);
return {
...content,
wordpress_status: wpStatus.wordpress_status,
};
} catch (error) {
console.warn(`Failed to fetch WP status for content ${content.id}:`, error);
return content;
}
}
return content;
})
);
setContent(resultsWithWPStatus);
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, publishStatusFilter, sortBy, sortDirection, searchTerm, 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 || 'created_at');
setSortDirection(direction);
setCurrentPage(1);
};
// Row action handler
const handleRowAction = useCallback(async (action: string, row: Content) => {
if (action === 'publish_wordpress') {
try {
const response = await fetchAPI('/v1/publisher/publish/', {
method: 'POST',
body: JSON.stringify({
content_id: row.id,
destinations: ['wordpress']
})
});
if (response.success) {
toast.success(`Published "${row.title}" to WordPress`);
loadContent();
} else {
toast.error(response.error || 'Failed to publish');
}
} catch (error: any) {
console.error('WordPress publish error:', error);
toast.error(`Failed to publish: ${error.message || 'Network error'}`);
}
} else 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 === 'edit') {
// Navigate to content editor (if exists) or show edit modal
navigate(`/writer/content?id=${row.id}`);
}
}, [toast, 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]);
// Bulk WordPress publish
const handleBulkPublishWordPress = useCallback(async (ids: string[]) => {
try {
const contentIds = ids.map(id => parseInt(id));
let successCount = 0;
let failedCount = 0;
// Publish each item individually
for (const contentId of contentIds) {
try {
const response = await fetchAPI('/v1/publisher/publish/', {
method: 'POST',
body: JSON.stringify({
content_id: contentId,
destinations: ['wordpress']
})
});
if (response.success) {
successCount++;
} else {
failedCount++;
console.warn(`Failed to publish content ${contentId}:`, response.error);
}
} catch (error) {
failedCount++;
console.error(`Error publishing content ${contentId}:`, error);
}
}
if (successCount > 0) {
toast.success(`Published ${successCount} item(s) to WordPress`);
}
if (failedCount > 0) {
toast.warning(`${failedCount} item(s) failed to publish`);
}
loadContent();
} catch (error: any) {
toast.error(`Failed to bulk publish: ${error.message}`);
throw error;
}
}, [toast, loadContent]);
// Bulk action handler
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
if (action === 'bulk_publish_wordpress') {
await handleBulkPublishWordPress(ids);
}
}, [handleBulkPublishWordPress]);
// Bulk status update handler
const handleBulkUpdateStatus = useCallback(async (ids: string[], status: string) => {
try {
const numIds = ids.map(id => parseInt(id));
// Note: This would need a backend endpoint like /v1/writer/content/bulk_update/
// For now, just show a toast
toast.info('Bulk status update functionality coming soon');
} catch (error: any) {
throw error;
}
}, [toast]);
// Bulk export handler
const handleBulkExport = useCallback(async (ids: string[]) => {
try {
if (!ids || ids.length === 0) {
throw new Error('No records selected for export');
}
toast.info('Export functionality coming soon');
} catch (error: any) {
throw error;
}
}, [toast]);
// Create page config
const pageConfig = useMemo(() => {
return createPublishedPageConfig({
searchTerm,
setSearchTerm,
statusFilter,
setStatusFilter,
publishStatusFilter,
setPublishStatusFilter,
setCurrentPage,
activeSector,
onRowClick: (row: Content) => {
navigate(`/writer/content/${row.id}`);
},
});
}, [searchTerm, statusFilter, publishStatusFilter, activeSector, navigate]);
// 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,
}));
}, [pageConfig?.headerMetrics, content, totalCount]);
// Writer navigation tabs
const writerTabs = [
{ label: 'Queue', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
];
return (
<>
<PageHeader
title="Published Articles"
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
/>
<TablePageTemplate
columns={pageConfig.columns}
data={content}
loading={loading}
showContent={showContent}
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
status: statusFilter,
publishStatus: publishStatusFilter,
}}
onFilterChange={(key: string, value: any) => {
if (key === 'search') {
setSearchTerm(value);
} else if (key === 'status') {
setStatusFilter(value);
setCurrentPage(1);
} else if (key === 'publishStatus') {
setPublishStatusFilter(value);
setCurrentPage(1);
}
}}
pagination={{
currentPage,
totalPages,
totalCount,
onPageChange: setCurrentPage,
}}
selection={{
selectedIds,
onSelectionChange: setSelectedIds,
}}
sorting={{
sortBy,
sortDirection,
onSort: handleSort,
}}
headerMetrics={headerMetrics}
onRowAction={handleRowAction}
onDelete={handleDelete}
onBulkDelete={handleBulkDelete}
onBulkAction={handleBulkAction}
onBulkUpdateStatus={handleBulkUpdateStatus}
onBulkExport={handleBulkExport}
getItemDisplayName={(row: Content) => row.title || `Content #${row.id}`}
/>
{/* Module Metrics Footer */}
<ModuleMetricsFooter
metrics={[
{
title: 'Published Content',
value: content.filter(c => c.status === 'published').length.toLocaleString(),
subtitle: `${content.filter(c => c.external_id).length} on WordPress`,
icon: <CheckCircleIcon className="w-5 h-5" />,
accentColor: 'green',
href: '/writer/published',
},
{
title: 'Draft Content',
value: content.filter(c => c.status === 'draft').length.toLocaleString(),
subtitle: 'Not yet published',
icon: <FileIcon className="w-5 h-5" />,
accentColor: 'blue',
},
]}
progress={{
label: 'WordPress Publishing Progress',
value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0,
color: 'success',
}}
/>
</>
);
}