/** * Keywords Page Configuration * Centralized config for Keywords page table, filters, and actions * * This config is fully dynamic - all columns, filters, actions, and form fields * are defined here instead of inline in the component. */ import React from 'react'; import { keywordColumn, volumeColumn, difficultyColumn, intentColumn, clusterColumn, sectorColumn, statusColumn, createdColumn, } from '../snippets/columns.snippets'; // Icons removed - bulkActions and rowActions are now in table-actions.config.tsx import Badge from '../../components/ui/badge/Badge'; import { getDifficultyNumber, getDifficultyOptions, getDifficultyValueFromNumber } from '../../utils/difficulty'; import { formatRelativeDate } from '../../utils/date'; import { Keyword } from '../../services/api'; import Input from '../../components/form/input/InputField'; import Label from '../../components/form/Label'; import Button from '../../components/ui/button/Button'; // SelectDropdown not used directly in config - removed // Type definitions 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) } // BulkActionConfig and RowActionConfig are now in table-actions.config.tsx export interface FormFieldConfig { key: string; label: string; type: 'text' | 'number' | 'select'; placeholder?: string; required?: boolean; min?: number; max?: number; className?: string; options?: Array<{ value: string; label: string }>; value: any; // Required for FormModal onChange: (value: any) => void; // Required for FormModal } export interface FilterConfig { key: string; label: string; type: 'text' | 'select' | 'daterange' | 'range' | 'custom'; placeholder?: string; options?: Array<{ value: string; label: string }>; min?: number; max?: number; step?: number; className?: string; customRender?: () => React.ReactNode; // For complex custom filters like volume range dynamicOptions?: string; // e.g., 'clusters' - flag for dynamic option loading } export interface HeaderMetricConfig { label: string; value: number; accentColor: 'blue' | 'green' | 'amber' | 'purple'; calculate: (data: { keywords: any[]; totalCount: number; clusters: any[] }) => number; } export interface KeywordsPageConfig { columns: ColumnConfig[]; filters: FilterConfig[]; // bulkActions and rowActions are now global - defined in table-actions.config.tsx formFields: (clusters: Array<{ id: number; name: string }>) => FormFieldConfig[]; headerMetrics: HeaderMetricConfig[]; exportConfig: { endpoint: string; filename: string; formats: Array<'csv' | 'json'>; }; importConfig: { endpoint: string; acceptedFormats: string[]; maxFileSize: number; }; } /** * Factory function to create keywords page config * Accepts handlers/closures from component for actions that need them */ export const createKeywordsPageConfig = ( handlers: { clusters: Array<{ id: number; name: string }>; activeSector: { id: number; name: string } | null; availableSeedKeywords: Array<{ id: number; keyword: string; volume: number; difficulty: number; intent: string }>; formData: { seed_keyword_id: number; volume_override?: number | null; difficulty_override?: number | null; cluster_id?: number | null; status: string; }; setFormData: React.Dispatch>; // Filter state handlers searchTerm: string; setSearchTerm: (value: string) => void; statusFilter: string; setStatusFilter: (value: string) => void; intentFilter: string; setIntentFilter: (value: string) => void; difficultyFilter: string; setDifficultyFilter: (value: string) => void; clusterFilter: string; setClusterFilter: (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; loadKeywords: () => Promise; } ): KeywordsPageConfig => { const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors return { columns: [ { ...keywordColumn, sortable: true, sortField: 'keyword', }, // Sector column - only show when viewing all sectors ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Keyword) => ( {row.sector_name || '-'} ), }] : []), { ...volumeColumn, sortable: true, sortField: 'volume', render: (value: number) => value.toLocaleString(), }, { ...clusterColumn, sortable: true, sortField: 'cluster_id', render: (_value: string, row: Keyword) => row.cluster_name || '-', }, { ...difficultyColumn, sortable: true, sortField: 'difficulty', align: 'center' as const, render: (value: number) => { const difficultyNum = getDifficultyNumber(value); const difficultyBadgeVariant = 'light'; 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 ); }, }, { ...intentColumn, sortable: true, sortField: 'intent', render: (value: string) => { // Map intent values to badge colors // Transactional and Commercial → success (green, like active) // Navigational → warning (amber/yellow, like pending) // Informational → info (blue) const getIntentColor = (intent: string) => { const lowerIntent = intent?.toLowerCase() || ''; if (lowerIntent === 'transactional' || lowerIntent === 'commercial') { return 'success'; // Green, like active status } else if (lowerIntent === 'navigational') { return 'warning'; // Amber/yellow, like pending status } return 'info'; // Blue for informational or default }; return ( {value} ); }, }, { ...statusColumn, sortable: true, sortField: 'status', render: (value: string) => { return ( {value} ); }, }, { ...createdColumn, sortable: true, sortField: 'created_at', render: (value: string) => formatRelativeDate(value), }, // Optional columns - hidden by default { key: 'updated_at', label: 'Updated', sortable: true, sortField: 'updated_at', defaultVisible: false, render: (value: string) => formatRelativeDate(value), }, { key: 'volume_override', label: 'Volume Override', sortable: true, sortField: 'volume_override', defaultVisible: false, render: (value: number | null) => value ? value.toLocaleString() : '-', }, { key: 'difficulty_override', label: 'Difficulty Override', sortable: true, sortField: 'difficulty_override', defaultVisible: false, align: 'center' as const, render: (value: number | null) => { if (value === null || value === undefined) return '-'; const difficultyNum = getDifficultyNumber(value); return typeof difficultyNum === 'number' ? ( {difficultyNum} ) : ( difficultyNum ); }, }, ], filters: [ { key: 'search', label: 'Search', type: 'text', placeholder: 'Search keywords...', }, { key: 'status', label: 'Status', type: 'select', options: [ { value: '', label: 'All Status' }, { value: 'active', label: 'Active' }, { value: 'pending', label: 'Pending' }, { value: 'archived', label: 'Archived' }, ], }, { key: 'intent', label: 'Intent', type: 'select', options: [ { value: '', label: 'All Intent' }, { value: 'informational', label: 'Informational' }, { value: 'navigational', label: 'Navigational' }, { value: 'transactional', label: 'Transactional' }, { value: 'commercial', label: 'Commercial' }, ], }, { 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" />
)}
), }, { key: 'cluster_id', label: 'Cluster', type: 'select', options: (() => { // Dynamically generate options from current clusters return [ { value: '', label: 'All Clusters' }, ...handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name })), ]; })(), className: 'w-40', }, ], headerMetrics: [ { label: 'Total Keywords', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, }, { label: 'Mapped', value: 0, accentColor: 'green' as const, calculate: (data) => data.keywords.filter((k: Keyword) => k.cluster_id).length, }, { label: 'Unmapped', value: 0, accentColor: 'amber' as const, calculate: (data) => data.keywords.filter((k: Keyword) => !k.cluster_id).length, }, { label: 'Total Volume', value: 0, accentColor: 'purple' as const, calculate: (data) => data.keywords.reduce((sum: number, k: Keyword) => sum + (k.volume || 0), 0), }, ], // bulkActions and rowActions are now global - defined in table-actions.config.ts // They're automatically loaded by TablePageTemplate based on the current route formFields: (clusters: Array<{ id: number; name: string }>) => [ { key: 'seed_keyword_id', label: 'Seed Keyword', type: 'select', placeholder: 'Select a seed keyword', value: handlers.formData.seed_keyword_id?.toString() || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, seed_keyword_id: value ? parseInt(value) : 0 }), required: true, options: [ { value: '', label: 'Select a keyword...' }, ...handlers.availableSeedKeywords.map((sk) => ({ value: sk.id.toString(), label: `${sk.keyword} (Vol: ${sk.volume.toLocaleString()}, Diff: ${sk.difficulty}, ${sk.intent})`, })), ], }, { key: 'volume_override', label: 'Volume Override (optional)', type: 'number', placeholder: 'Leave empty to use seed keyword volume', value: handlers.formData.volume_override ?? '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, volume_override: value ? parseInt(value) : null }), }, { key: 'difficulty_override', label: 'Difficulty Override (optional)', type: 'number', placeholder: 'Leave empty to use seed keyword difficulty', value: handlers.formData.difficulty_override ?? '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, difficulty_override: value ? parseInt(value) : null }), min: 0, max: 100, }, { key: 'cluster_id', label: 'Cluster', type: 'select', value: handlers.formData.cluster_id?.toString() || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, cluster_id: value ? parseInt(value) : null, }), options: [ { value: '', label: 'No Cluster' }, ...clusters.map((c) => ({ value: c.id.toString(), label: c.name })), ], }, { key: 'status', label: 'Status', type: 'select', value: handlers.formData.status || 'pending', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, status: value }), options: [ { value: 'pending', label: 'Pending' }, { value: 'active', label: 'Active' }, { value: 'archived', label: 'Archived' }, ], }, ], exportConfig: { endpoint: '/v1/planner/keywords/export/', filename: 'keywords', formats: ['csv', 'json'], }, importConfig: { endpoint: '/v1/planner/keywords/import_keywords/', acceptedFormats: ['.csv'], maxFileSize: 5 * 1024 * 1024, // 5MB }, }; };