Enhance Content Management: Add sector name to ContentSerializer, improve Content view with pagination and search filters, and refactor Content page for better data handling and display.
This commit is contained in:
247
frontend/src/config/pages/content.config.tsx
Normal file
247
frontend/src/config/pages/content.config.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Content Page Configuration
|
||||
* Centralized config for Content page table, filters, and actions
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
titleColumn,
|
||||
statusColumn,
|
||||
createdColumn,
|
||||
wordCountColumn,
|
||||
sectorColumn,
|
||||
} from '../snippets/columns.snippets';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { formatRelativeDate } from '../../utils/date';
|
||||
import { Content } from '../../services/api';
|
||||
|
||||
export interface ColumnConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
sortable?: boolean;
|
||||
sortField?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
width?: string;
|
||||
render?: (value: any, row: any) => React.ReactNode;
|
||||
toggleable?: boolean;
|
||||
toggleContentKey?: string;
|
||||
toggleContentLabel?: string;
|
||||
}
|
||||
|
||||
export interface FilterConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'text' | 'select';
|
||||
placeholder?: string;
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
}
|
||||
|
||||
export interface HeaderMetricConfig {
|
||||
label: string;
|
||||
value: number;
|
||||
accentColor: 'blue' | 'green' | 'amber' | 'purple';
|
||||
calculate: (data: { content: any[]; totalCount: number }) => number;
|
||||
}
|
||||
|
||||
export interface ContentPageConfig {
|
||||
columns: ColumnConfig[];
|
||||
filters: FilterConfig[];
|
||||
headerMetrics: HeaderMetricConfig[];
|
||||
}
|
||||
|
||||
const getList = (primary?: string[], fallback?: any): string[] => {
|
||||
if (primary && primary.length > 0) return primary;
|
||||
if (!fallback) return [];
|
||||
if (Array.isArray(fallback)) return fallback;
|
||||
return [];
|
||||
};
|
||||
|
||||
const renderBadgeList = (items: string[], emptyLabel = '-') => {
|
||||
if (!items || items.length === 0) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">{emptyLabel}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{items.map((item, index) => (
|
||||
<Badge key={`${item}-${index}`} color="light" size="sm" variant="light">
|
||||
{item}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const createContentPageConfig = (
|
||||
handlers: {
|
||||
activeSector: { id: number; name: string } | null;
|
||||
searchTerm: string;
|
||||
setSearchTerm: (value: string) => void;
|
||||
statusFilter: string;
|
||||
setStatusFilter: (value: string) => void;
|
||||
setCurrentPage: (page: number) => void;
|
||||
}
|
||||
): ContentPageConfig => {
|
||||
const showSectorColumn = !handlers.activeSector;
|
||||
|
||||
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
|
||||
draft: 'warning',
|
||||
review: 'info',
|
||||
publish: 'success',
|
||||
};
|
||||
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
...titleColumn,
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
toggleable: true,
|
||||
toggleContentKey: 'html_content',
|
||||
toggleContentLabel: 'Generated Content',
|
||||
render: (value: string, row: Content) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{row.meta_title || row.title || row.task_title || `Task #${row.task}`}
|
||||
</div>
|
||||
{row.meta_description && (
|
||||
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||
{row.meta_description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...(showSectorColumn ? [{
|
||||
...sectorColumn,
|
||||
render: (value: string, row: Content) => (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{row.sector_name || '-'}
|
||||
</Badge>
|
||||
),
|
||||
}] : []),
|
||||
{
|
||||
key: 'primary_keyword',
|
||||
label: 'Primary Keyword',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
render: (value: string, row: Content) => (
|
||||
row.primary_keyword ? (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{row.primary_keyword}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'secondary_keywords',
|
||||
label: 'Secondary Keywords',
|
||||
sortable: false,
|
||||
width: '200px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const secondaryKeywords = getList(
|
||||
row.secondary_keywords,
|
||||
row.metadata?.secondary_keywords
|
||||
);
|
||||
return renderBadgeList(secondaryKeywords);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
label: 'Tags',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const tags = getList(row.tags, row.metadata?.tags);
|
||||
return renderBadgeList(tags);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'categories',
|
||||
label: 'Categories',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const categories = getList(row.categories, row.metadata?.categories);
|
||||
return renderBadgeList(categories);
|
||||
},
|
||||
},
|
||||
{
|
||||
...wordCountColumn,
|
||||
sortable: true,
|
||||
sortField: 'word_count',
|
||||
render: (value: number) => value?.toLocaleString() ?? '-',
|
||||
},
|
||||
{
|
||||
...statusColumn,
|
||||
sortable: true,
|
||||
sortField: 'status',
|
||||
render: (value: string) => {
|
||||
const status = value || 'draft';
|
||||
const color = statusColors[status] || 'primary';
|
||||
const label = status.replace('_', ' ').replace(/^\w/, (c) => c.toUpperCase());
|
||||
return (
|
||||
<Badge color={color} size="sm" variant="light">
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...createdColumn,
|
||||
sortable: true,
|
||||
sortField: 'generated_at',
|
||||
label: 'Generated',
|
||||
render: (value: string) => formatRelativeDate(value),
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'search',
|
||||
label: 'Search',
|
||||
type: 'text',
|
||||
placeholder: 'Search content...',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Status' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'review', label: 'Review' },
|
||||
{ value: 'publish', label: 'Publish' },
|
||||
],
|
||||
},
|
||||
],
|
||||
headerMetrics: [
|
||||
{
|
||||
label: 'Total Content',
|
||||
value: 0,
|
||||
accentColor: 'blue' as const,
|
||||
calculate: (data) => data.totalCount || 0,
|
||||
},
|
||||
{
|
||||
label: 'Draft',
|
||||
value: 0,
|
||||
accentColor: 'warning' as const,
|
||||
calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length,
|
||||
},
|
||||
{
|
||||
label: 'Review',
|
||||
value: 0,
|
||||
accentColor: 'info' as const,
|
||||
calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length,
|
||||
},
|
||||
{
|
||||
label: 'Published',
|
||||
value: 0,
|
||||
accentColor: 'success' as const,
|
||||
calculate: (data) => data.content.filter((c: Content) => c.status === 'publish').length,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -100,8 +100,8 @@ export const createTasksPageConfig = (
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
toggleable: true,
|
||||
toggleContentKey: 'content_html',
|
||||
toggleContentLabel: 'Generated Content',
|
||||
toggleContentKey: 'description',
|
||||
toggleContentLabel: 'Idea & Content Outline',
|
||||
},
|
||||
// Sector column - only show when viewing all sectors
|
||||
...(showSectorColumn ? [{
|
||||
|
||||
Reference in New Issue
Block a user