/** * 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, }, ], }; };