Files
igny8/frontend/src/config/pages/content.config.tsx
2025-11-11 18:34:58 +00:00

314 lines
9.4 KiB
TypeScript

/**
* 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 { FileIcon, MoreDotIcon } from '../../icons';
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_id}`}
</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',
align: 'right',
render: (value: string, row: Content) => {
const hasPrompts = row.has_image_prompts || false;
const hasImages = row.has_generated_images || false;
return (
<div className="flex items-center justify-end gap-3 pr-10">
<span className="text-gray-700 dark:text-gray-300 whitespace-nowrap">
{formatRelativeDate(value)}
</span>
<div className="flex items-center gap-2">
{/* Prompt Icon */}
<div
className={`w-5 h-5 flex items-center justify-center flex-shrink-0 ${
hasPrompts
? 'text-green-500 dark:text-green-400'
: 'text-gray-300 dark:text-gray-600'
}`}
title={hasPrompts ? 'Image prompts generated' : 'No image prompts'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-4 h-4"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
</div>
{/* Image Icon */}
<div
className={`w-5 h-5 flex items-center justify-center flex-shrink-0 ${
hasImages
? 'text-green-500 dark:text-green-400'
: 'text-gray-300 dark:text-gray-600'
}`}
title={hasImages ? 'Images generated' : 'No images generated'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-4 h-4"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
</div>
</div>
);
},
},
],
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,
},
],
};
};