/** * Clusters Page Configuration * Centralized config for Clusters page table, filters, and actions */ import React from 'react'; import { Link } from 'react-router-dom'; import { titleColumn, sectorColumn, difficultyColumn, statusColumn, createdWithActionsColumn, } from '../snippets/columns.snippets'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { getDifficultyNumber } from '../../utils/difficulty'; import { Cluster } from '../../services/api'; import Input from '../../components/form/input/InputField'; import Label from '../../components/form/Label'; import Button from '../../components/ui/button/Button'; import { BoltIcon } 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; defaultVisible?: boolean; // Whether column is visible by default (default: true) } export interface FormFieldConfig { key: string; label: string; type: 'text' | 'number' | 'select' | 'textarea'; placeholder?: string; required?: boolean; value: any; onChange: (value: any) => void; } export interface FilterConfig { key: string; label: string; type: 'text' | 'select' | 'custom'; placeholder?: string; options?: Array<{ value: string; label: string }>; customRender?: () => React.ReactNode; } export interface HeaderMetricConfig { label: string; value: number; accentColor: 'blue' | 'green' | 'amber' | 'purple'; calculate: (data: { clusters: any[]; totalCount: number }) => number; } export interface ClustersPageConfig { columns: ColumnConfig[]; filters: FilterConfig[]; formFields: () => FormFieldConfig[]; headerMetrics: HeaderMetricConfig[]; } export const createClustersPageConfig = ( handlers: { activeSector: { id: number; name: string } | null; formData: { name: string; description?: string | null; status: string; }; setFormData: React.Dispatch>; searchTerm: string; setSearchTerm: (value: string) => void; statusFilter: string; setStatusFilter: (value: string) => void; difficultyFilter: string; setDifficultyFilter: (value: string) => void; volumeMin: number | ''; volumeMax: number | ''; setVolumeMin: (value: number | '') => void; setVolumeMax: (value: number | '') => void; isVolumeDropdownOpen: boolean; setIsVolumeDropdownOpen: (value: boolean) => void; tempVolumeMin: number | ''; tempVolumeMax: number | ''; setTempVolumeMin: (value: number | '') => void; setTempVolumeMax: (value: number | '') => void; volumeButtonRef: React.RefObject; volumeDropdownRef: React.RefObject; setCurrentPage: (page: number) => void; loadClusters: () => Promise; onGenerateIdeas?: (clusterId: number) => void; // Handler for generate ideas button } ): ClustersPageConfig => { const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors return { columns: [ { ...titleColumn, key: 'name', label: 'Cluster Name', sortable: true, sortField: 'name', width: '400px', render: (value: string, row: Cluster) => ( {value} ), }, // Sector column - only show when viewing all sectors ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Cluster) => ( {row.sector_name || '-'} ), }] : []), { key: 'keywords_count', label: 'KW Count', sortable: true, sortField: 'keywords_count', align: 'center' as const, headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { key: 'volume', label: 'Volume', sortable: true, sortField: 'volume', align: 'center' as const, headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { ...difficultyColumn, key: 'difficulty', label: 'Difficulty', sortable: true, sortField: 'difficulty', align: 'center' as const, headingAlign: 'center' as const, render: (value: number) => { const difficultyNum = getDifficultyNumber(value); const difficultyBadgeColor = typeof difficultyNum === 'number' && difficultyNum === 1 ? 'success' : typeof difficultyNum === 'number' && difficultyNum === 2 ? 'success' : typeof difficultyNum === 'number' && difficultyNum === 3 ? 'warning' : typeof difficultyNum === 'number' && difficultyNum === 4 ? 'error' : typeof difficultyNum === 'number' && difficultyNum === 5 ? 'error' : 'light'; return typeof difficultyNum === 'number' ? ( {difficultyNum} ) : ( difficultyNum ); }, }, { key: 'content_count', label: 'Content', sortable: false, // Backend doesn't support sorting by content_count sortField: 'content_count', align: 'center' as const, headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { ...statusColumn, sortable: true, sortField: 'status', render: (value: string) => { const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-'; return ( {properCase} ); }, }, { key: 'ideas_count', label: 'Ideas', sortable: false, // Backend doesn't support sorting by ideas_count sortField: 'ideas_count', align: 'center' as const, headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, // Generate Ideas action column - only shows button for status = 'new' { key: 'generate_action', label: 'Generate Ideas', sortable: false, render: (_value: any, row: Cluster) => { // Only show generate button for clusters with status 'new' if (row.status === 'new' && handlers.onGenerateIdeas) { return ( ); } return -; }, }, { ...createdWithActionsColumn, sortable: true, sortField: 'created_at', width: '130px', render: (value: string) => formatRelativeDate(value), }, // Optional columns - hidden by default { key: 'description', label: 'Description', sortable: false, defaultVisible: false, width: '250px', render: (value: string | null) => ( {value || '-'} ), }, { key: 'mapped_pages', label: 'Mapped Pages', sortable: false, // Backend doesn't support sorting by mapped_pages sortField: 'mapped_pages', defaultVisible: false, width: '120px', render: (value: number) => value.toLocaleString(), }, { key: 'updated_at', label: 'Modified', sortable: false, // Backend doesn't support sorting by updated_at sortField: 'updated_at', defaultVisible: false, render: (value: string) => formatRelativeDate(value), }, ], filters: [ { key: 'search', label: 'Search', type: 'text', placeholder: 'Search clusters...', }, { key: 'status', label: 'Status', type: 'select', options: [ { value: '', label: 'All Status' }, { value: 'new', label: 'New' }, { value: 'mapped', label: 'Mapped' }, ], }, { key: 'difficulty', label: 'Difficulty', type: 'select', options: [ { value: '', label: 'All Difficulty' }, { value: '1', label: '1 - Very Easy' }, { value: '2', label: '2 - Easy' }, { value: '3', label: '3 - Medium' }, { value: '4', label: '4 - Hard' }, { value: '5', label: '5 - Very Hard' }, ], }, { key: 'volume', label: 'Volume Range', type: 'custom', customRender: () => (
{/* Dropdown Menu */} {handlers.isVolumeDropdownOpen && (
{ const val = e.target.value; handlers.setTempVolumeMin(val === '' ? '' : parseInt(val) || ''); }} className="w-full h-9" />
{ const val = e.target.value; handlers.setTempVolumeMax(val === '' ? '' : parseInt(val) || ''); }} className="w-full h-9" />
)}
), }, ], formFields: () => [ { key: 'name', label: 'Cluster Name', type: 'text', placeholder: 'Enter cluster name', required: true, value: handlers.formData.name || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, name: value }), }, { key: 'description', label: 'Description', type: 'textarea', placeholder: 'Enter cluster description', value: handlers.formData.description || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, description: value }), }, { key: 'status', label: 'Status', type: 'select', value: handlers.formData.status || 'new', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, status: value }), options: [ { value: 'new', label: 'New' }, { value: 'mapped', label: 'Mapped' }, ], }, ], headerMetrics: [ { label: 'Clusters', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, tooltip: 'Topic clusters organizing your keywords. Each cluster should have 3-7 related keywords.', }, { label: 'New', value: 0, accentColor: 'amber' as const, calculate: (data) => data.clusters.filter((c: Cluster) => (c.ideas_count || 0) === 0).length, tooltip: 'Clusters without content ideas yet. Generate ideas for these clusters to move them into the pipeline.', }, { label: 'Keywords', value: 0, accentColor: 'purple' as const, calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.keywords_count || 0), 0), tooltip: 'Total keywords organized across all clusters. More keywords = better topic coverage.', }, { label: 'Volume', value: 0, accentColor: 'green' as const, calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.volume || 0), 0), tooltip: 'Combined search volume across all clusters. Prioritize high-volume clusters for maximum traffic.', }, ], }; };