300 lines
9.0 KiB
TypeScript
300 lines
9.0 KiB
TypeScript
/**
|
|
* Review Page Configuration
|
|
* Centralized config for Review page table, filters, and actions
|
|
*/
|
|
|
|
import { Content } from '../../services/api';
|
|
import Badge from '../../components/ui/badge/Badge';
|
|
import { formatRelativeDate } from '../../utils/date';
|
|
import { CheckCircleIcon } from '../../icons';
|
|
import { STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping';
|
|
|
|
export interface ColumnConfig {
|
|
key: string;
|
|
label: string;
|
|
sortable?: boolean;
|
|
sortField?: string;
|
|
align?: 'left' | 'center' | 'right';
|
|
width?: string;
|
|
numeric?: boolean;
|
|
date?: boolean;
|
|
render?: (value: any, row: any) => React.ReactNode;
|
|
toggleable?: boolean;
|
|
toggleContentKey?: string;
|
|
toggleContentLabel?: string;
|
|
defaultVisible?: boolean;
|
|
}
|
|
|
|
export interface FilterConfig {
|
|
key: string;
|
|
label: string;
|
|
type: 'text' | 'select';
|
|
placeholder?: string;
|
|
options?: Array<{ value: string; label: string }>;
|
|
}
|
|
|
|
export interface HeaderMetricConfig {
|
|
label: string;
|
|
accentColor: 'blue' | 'green' | 'amber' | 'purple';
|
|
calculate: (data: { content: Content[]; totalCount: number }) => number;
|
|
}
|
|
|
|
export interface ReviewPageConfig {
|
|
columns: ColumnConfig[];
|
|
filters: FilterConfig[];
|
|
headerMetrics: HeaderMetricConfig[];
|
|
}
|
|
|
|
export function createReviewPageConfig(params: {
|
|
searchTerm: string;
|
|
setSearchTerm: (value: string) => void;
|
|
statusFilter: string;
|
|
setStatusFilter: (value: string) => void;
|
|
setCurrentPage: (page: number) => void;
|
|
activeSector: { id: number; name: string } | null;
|
|
onRowClick?: (row: Content) => void;
|
|
}): ReviewPageConfig {
|
|
const showSectorColumn = !params.activeSector;
|
|
|
|
const columns: ColumnConfig[] = [
|
|
{
|
|
key: 'title',
|
|
label: 'Content Idea Title',
|
|
sortable: true,
|
|
sortField: 'title',
|
|
width: '400px',
|
|
render: (value: string, row: Content) => (
|
|
<div className="flex items-center gap-2">
|
|
{params.onRowClick ? (
|
|
<button
|
|
onClick={() => params.onRowClick!(row)}
|
|
className="text-sm text-brand-500 hover:text-brand-600 hover:underline text-left transition-colors"
|
|
>
|
|
{value || `Content #${row.id}`}
|
|
</button>
|
|
) : (
|
|
<span className="text-sm text-gray-900 dark:text-white">
|
|
{value || `Content #${row.id}`}
|
|
</span>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'categories',
|
|
label: 'Categories',
|
|
sortable: false,
|
|
render: (_value: any, row: Content) => {
|
|
const categories = row.categories || [];
|
|
if (!categories || categories.length === 0) {
|
|
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
|
|
}
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{categories.slice(0, 2).map((category, index) => (
|
|
<Badge key={`${category}-${index}`} color="blue" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{category}</span>
|
|
</Badge>
|
|
))}
|
|
{categories.length > 2 && (
|
|
<span className="text-[11px] text-gray-500">+{categories.length - 2}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'tags',
|
|
label: 'Tags',
|
|
sortable: false,
|
|
render: (_value: any, row: Content) => {
|
|
const tags = row.tags || [];
|
|
if (!tags || tags.length === 0) {
|
|
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
|
|
}
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{tags.slice(0, 2).map((tag, index) => (
|
|
<Badge key={`${tag}-${index}`} color="pink" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{tag}</span>
|
|
</Badge>
|
|
))}
|
|
{tags.length > 2 && (
|
|
<span className="text-[11px] text-gray-500">+{tags.length - 2}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'content_type',
|
|
label: 'Type',
|
|
sortable: true,
|
|
sortField: 'content_type',
|
|
render: (value: string) => {
|
|
const label = TYPE_LABELS[value] || value || '-';
|
|
const properCase = label.charAt(0).toUpperCase() + label.slice(1);
|
|
return (
|
|
<Badge color="blue" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{properCase}</span>
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'content_structure',
|
|
label: 'Structure',
|
|
sortable: true,
|
|
sortField: 'content_structure',
|
|
render: (value: string) => {
|
|
const label = STRUCTURE_LABELS[value] || value || '-';
|
|
const properCase = label.split(/[_\s]+/).map(word =>
|
|
word.charAt(0).toUpperCase() + word.slice(1)
|
|
).join(' ');
|
|
return (
|
|
<Badge color="purple" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{properCase}</span>
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'cluster_name',
|
|
label: 'Cluster',
|
|
sortable: false,
|
|
render: (_value: any, row: Content) => {
|
|
const clusterName = row.cluster_name;
|
|
if (!clusterName) {
|
|
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
|
}
|
|
return (
|
|
<span className="text-gray-800 dark:text-white">
|
|
{clusterName}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
sortable: true,
|
|
sortField: 'status',
|
|
render: (value: string, row: Content) => {
|
|
const status = value || 'draft';
|
|
const statusColors: Record<string, 'gray' | 'blue' | 'green' | 'amber' | 'red'> = {
|
|
draft: 'gray',
|
|
review: 'blue',
|
|
published: 'green',
|
|
scheduled: 'amber',
|
|
archived: 'red',
|
|
};
|
|
const color = statusColors[status] || 'gray';
|
|
const label = status.charAt(0).toUpperCase() + status.slice(1);
|
|
|
|
return (
|
|
<div className="flex items-center gap-1.5">
|
|
<Badge color={color} size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{label}</span>
|
|
</Badge>
|
|
{row.external_id && (
|
|
<CheckCircleIcon className="w-3.5 h-3.5 text-success-500" title="Published to Site" />
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'word_count',
|
|
label: 'Words',
|
|
sortable: true,
|
|
sortField: 'word_count',
|
|
numeric: true,
|
|
align: 'center' as const,
|
|
headingAlign: 'center' as const,
|
|
render: (value: number) => (
|
|
<span className="font-mono text-sm text-gray-700 dark:text-gray-300">
|
|
{value?.toLocaleString() || 0}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'created_at',
|
|
label: 'Created',
|
|
sortable: true,
|
|
sortField: 'created_at',
|
|
date: true,
|
|
width: '130px',
|
|
hasActions: true,
|
|
render: (value: string) => (
|
|
<span className="text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
|
{formatRelativeDate(value)}
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
if (showSectorColumn) {
|
|
columns.splice(4, 0, {
|
|
key: 'sector_name',
|
|
label: 'Sector',
|
|
sortable: false,
|
|
width: '120px',
|
|
render: (value: string, row: Content) => (
|
|
<Badge color="info" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
|
|
</Badge>
|
|
),
|
|
});
|
|
}
|
|
|
|
return {
|
|
columns,
|
|
filters: [
|
|
{
|
|
key: 'search',
|
|
label: 'Search',
|
|
type: 'text',
|
|
placeholder: 'Search content...',
|
|
},
|
|
],
|
|
headerMetrics: [
|
|
{
|
|
label: 'Content',
|
|
accentColor: 'blue',
|
|
calculate: ({ totalCount }) => totalCount,
|
|
tooltip: 'Total content items tracked. Overall volume across all stages.',
|
|
},
|
|
{
|
|
label: 'Draft',
|
|
accentColor: 'amber',
|
|
calculate: ({ content }) => content.filter(c => c.status === 'draft').length,
|
|
tooltip: 'Content written, images not generated. Generate images to move to review.',
|
|
},
|
|
{
|
|
label: 'In Review',
|
|
accentColor: 'purple',
|
|
calculate: ({ content }) => content.filter(c => c.status === 'review').length,
|
|
tooltip: 'Images generated, awaiting approval. Review and approve to publish.',
|
|
},
|
|
{
|
|
label: 'Approved',
|
|
accentColor: 'green',
|
|
calculate: ({ content }) => content.filter(c => c.status === 'approved').length,
|
|
tooltip: 'Approved content awaiting publishing. Publish to site when ready.',
|
|
},
|
|
{
|
|
label: 'Published',
|
|
accentColor: 'green',
|
|
calculate: ({ content }) => content.filter(c => c.status === 'published').length,
|
|
tooltip: 'Live content on your website. Successfully published and accessible.',
|
|
},
|
|
{
|
|
label: 'Total Images',
|
|
accentColor: 'blue',
|
|
calculate: ({ content }) => content.filter(c => c.has_generated_images).length,
|
|
tooltip: 'Total images generated across all content. Tracks visual asset coverage.',
|
|
},
|
|
],
|
|
};
|
|
}
|