252 lines
7.7 KiB
TypeScript
252 lines
7.7 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,
|
|
} from '../../services/api';
|
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
|
import { FileIcon } 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';
|
|
|
|
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('');
|
|
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>('generated_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);
|
|
|
|
// Load content - wrapped in useCallback
|
|
const loadContent = useCallback(async () => {
|
|
setLoading(true);
|
|
setShowContent(false);
|
|
try {
|
|
const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-generated_at';
|
|
|
|
const filters: ContentFilters = {
|
|
...(searchTerm && { search: searchTerm }),
|
|
...(statusFilter && { status: statusFilter }),
|
|
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);
|
|
};
|
|
|
|
// Create page config
|
|
const pageConfig = useMemo(() => {
|
|
return createContentPageConfig({
|
|
activeSector,
|
|
searchTerm,
|
|
setSearchTerm,
|
|
statusFilter,
|
|
setStatusFilter,
|
|
setCurrentPage,
|
|
});
|
|
}, [
|
|
activeSector,
|
|
searchTerm,
|
|
statusFilter,
|
|
]);
|
|
|
|
// 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]);
|
|
|
|
const handleRowAction = useCallback(async (action: string, row: ContentType) => {
|
|
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 generated: ${result.prompts_created || 0} prompt${(result.prompts_created || 0) === 1 ? '' : 's'} created`);
|
|
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}`);
|
|
}
|
|
}
|
|
}, [toast, progressModal, loadContent]);
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title="Content"
|
|
badge={{ icon: <FileIcon />, color: 'purple' }}
|
|
/>
|
|
<TablePageTemplate
|
|
columns={pageConfig.columns}
|
|
data={content}
|
|
loading={loading}
|
|
showContent={showContent}
|
|
filters={pageConfig.filters}
|
|
filterValues={{
|
|
search: searchTerm,
|
|
status: statusFilter,
|
|
}}
|
|
onFilterChange={(key: string, value: any) => {
|
|
if (key === 'search') {
|
|
setSearchTerm(value);
|
|
} else if (key === 'status') {
|
|
setStatusFilter(value);
|
|
setCurrentPage(1);
|
|
}
|
|
}}
|
|
pagination={{
|
|
currentPage,
|
|
totalPages,
|
|
totalCount,
|
|
onPageChange: setCurrentPage,
|
|
}}
|
|
sorting={{
|
|
sortBy,
|
|
sortDirection,
|
|
onSort: handleSort,
|
|
}}
|
|
selection={{
|
|
selectedIds,
|
|
onSelectionChange: setSelectedIds,
|
|
}}
|
|
headerMetrics={headerMetrics}
|
|
onRowAction={handleRowAction}
|
|
getItemDisplayName={(row: ContentType) => row.meta_title || row.title || `Content #${row.id}`}
|
|
/>
|
|
|
|
{/* 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);
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|