/**
* 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';
import { CONTENT_TYPE_OPTIONS, STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping';
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;
defaultVisible?: boolean; // Whether column is visible by default (default: true)
}
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 {emptyLabel};
}
return (
{items.map((item, index) => (
{item}
))}
);
};
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;
onRowClick?: (row: Content) => void;
}
): ContentPageConfig => {
const showSectorColumn = !handlers.activeSector;
const statusColors: Record = {
draft: 'warning',
review: 'info',
publish: 'success',
};
return {
columns: [
{
...titleColumn,
sortable: true,
sortField: 'title',
render: (value: string, row: Content) => (
{handlers.onRowClick ? (
) : (
{row.title || `Content #${row.id}`}
)}
),
},
...(showSectorColumn ? [{
...sectorColumn,
render: (value: string, row: Content) => (
{row.sector_name || '-'}
),
}] : []),
{
key: 'content_type',
label: 'Type',
sortable: true,
sortField: 'content_type',
width: '110px',
render: (value: string) => {
const label = TYPE_LABELS[value] || value || '-';
// Proper case: capitalize first letter only
const properCase = label.charAt(0).toUpperCase() + label.slice(1);
return (
{properCase}
);
},
},
{
key: 'content_structure',
label: 'Structure',
sortable: true,
sortField: 'content_structure',
width: '130px',
render: (value: string) => {
const label = STRUCTURE_LABELS[value] || value || '-';
// Proper case: capitalize first letter of each word
const properCase = label.split(/[_\s]+/).map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
return (
{properCase}
);
},
},
{
key: 'cluster_name',
label: 'Cluster',
sortable: false,
width: '130px',
render: (_value: any, row: Content) => {
const clusterName = row.cluster_name;
if (!clusterName) {
return -;
}
return (
{clusterName}
);
},
},
{
key: 'taxonomy_terms',
label: 'Tags',
sortable: false,
width: '150px',
render: (_value: any, row: Content) => {
const taxonomyTerms = row.taxonomy_terms;
if (!taxonomyTerms || taxonomyTerms.length === 0) {
return -;
}
return (
{taxonomyTerms.slice(0, 2).map((term) => (
{term.name}
))}
{taxonomyTerms.length > 2 && (
+{taxonomyTerms.length - 2}
)}
);
},
},
{
...statusColumn,
sortable: true,
sortField: 'status',
render: (value: string) => {
const statusColors: Record = {
draft: 'amber',
published: 'success',
};
const color = statusColors[value] || 'amber';
// Proper case
const label = value ? value.charAt(0).toUpperCase() + value.slice(1) : 'Draft';
return (
{label}
);
},
},
// Removed the separate Status icon column. Both icons will be shown in the Created column below.
{
key: 'source',
label: 'Source',
sortable: true,
sortField: 'source',
width: '90px',
render: (value: any, row: Content) => {
const source = value || row.source || 'igny8';
const sourceColors: Record = {
igny8: 'teal',
wordpress: 'cyan',
};
const sourceLabels: Record = {
igny8: 'Igny8',
wordpress: 'Wp',
};
return (
{sourceLabels[source] || source}
);
},
},
{
key: 'external_url',
label: 'URL',
sortable: false,
width: '200px',
defaultVisible: false,
render: (value: string | null, row: Content) => {
const url = value || row.external_url || null;
return url ? (
{url}
) : (
-
);
},
},
{
...createdColumn,
sortable: true,
sortField: 'created_at',
label: 'Created',
align: 'right',
render: (value: string, row: Content) => {
// Prompt icon logic (unchanged)
const hasPrompts = row.has_image_prompts || false;
// Image icon logic (status-aware)
let imageStatus: 'pending' | 'generated' | 'failed' | null = null;
if (row.image_status === 'failed') {
imageStatus = 'failed';
} else if (row.image_status === 'generated' || row.has_generated_images) {
imageStatus = 'generated';
} else if (row.has_image_prompts) {
imageStatus = 'pending';
}
const imageStatusColors: Record = {
'pending': 'text-amber-500 dark:text-amber-400',
'generated': 'text-green-500 dark:text-green-400',
'failed': 'text-red-500 dark:text-red-400',
};
const imageStatusTitles: Record = {
'pending': 'Images pending',
'generated': 'Images generated',
'failed': 'Image generation failed',
};
return (
{formatRelativeDate(value)}
{/* Prompts Icon */}
{/* Images Icon (status-aware) */}
);
},
},
// Optional columns - hidden by default
{
key: 'updated_at',
label: 'Updated',
sortable: true,
sortField: 'updated_at',
defaultVisible: false,
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: 'published', label: 'Published' },
],
},
{
key: 'content_type',
label: 'Content Type',
type: 'select',
options: [
{ value: '', label: 'All Types' },
...CONTENT_TYPE_OPTIONS,
],
},
{
key: 'content_structure',
label: 'Content Structure',
type: 'select',
options: [
{ value: '', label: 'All Structures' },
{ value: 'article', label: 'Article' },
{ value: 'guide', label: 'Guide' },
{ value: 'comparison', label: 'Comparison' },
{ value: 'review', label: 'Review' },
{ value: 'listicle', label: 'Listicle' },
{ value: 'landing_page', label: 'Landing Page' },
{ value: 'business_page', label: 'Business Page' },
{ value: 'service_page', label: 'Service Page' },
{ value: 'general', label: 'General' },
{ value: 'cluster_hub', label: 'Cluster Hub' },
{ value: 'product_page', label: 'Product Page' },
{ value: 'category_archive', label: 'Category Archive' },
{ value: 'tag_archive', label: 'Tag Archive' },
{ value: 'attribute_archive', label: 'Attribute Archive' },
],
},
{
key: 'source',
label: 'Source',
type: 'select',
options: [
{ value: '', label: 'All Sources' },
{ value: 'igny8', label: 'IGNY8' },
{ value: 'wordpress', label: 'WordPress' },
],
},
],
headerMetrics: [
{
label: 'Total Content',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
},
{
label: 'Draft',
value: 0,
accentColor: 'amber' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length,
},
{
label: 'Published',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'published').length,
},
],
};
};