/** * 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, countryColumn, clusterColumn, sectorColumn, statusColumn, createdWithActionsColumn, } 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; formData: { keyword?: string; volume?: number | null; difficulty?: number | null; country?: string; cluster_id?: number | null; status: string; }; setFormData: React.Dispatch>; // Filter state handlers searchTerm: string; setSearchTerm: (value: string) => void; statusFilter: string; setStatusFilter: (value: string) => void; countryFilter: string; setCountryFilter: (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: false, // Backend doesn't support sorting by keyword field sortField: 'seed_keyword__keyword', width: '300px', render: (value: string) => ( {value || '-'} ), }, // Sector column - only show when viewing all sectors ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Keyword) => ( {row.sector_name || '-'} ), }] : []), { ...volumeColumn, sortable: true, sortField: 'seed_keyword__volume', // Backend expects seed_keyword__volume align: 'center' as const, headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { ...clusterColumn, sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', width: '300px', render: (_value: string, row: Keyword) => row.cluster_name ? ( {row.cluster_name} ) : -, }, { ...difficultyColumn, sortable: true, sortField: 'seed_keyword__difficulty', // Backend expects seed_keyword__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 ? 'amber' : typeof difficultyNum === 'number' && difficultyNum === 4 ? 'error' : typeof difficultyNum === 'number' && difficultyNum === 5 ? 'error' : 'gray'; return typeof difficultyNum === 'number' ? ( {difficultyNum} ) : ( difficultyNum ); }, }, { ...countryColumn, sortable: false, // Backend doesn't support sorting by country sortField: 'seed_keyword__country', align: 'center' as const, headingAlign: 'center' as const, render: (value: string) => ( {value || '-'} ), }, { ...statusColumn, sortable: true, sortField: 'status', render: (value: string) => { const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-'; return ( {properCase} ); }, }, { ...createdWithActionsColumn, label: 'Added', sortable: true, sortField: 'created_at', width: '130px', render: (value: string) => formatRelativeDate(value), }, ], filters: [ { key: 'search', label: 'Search', type: 'text', placeholder: 'Search keywords...', }, { key: 'status', label: 'Status', type: 'select', options: [ { value: '', label: 'All Status' }, { value: 'new', label: 'New' }, { value: 'mapped', label: 'Mapped' }, ], }, { key: 'country', label: 'Country', type: 'select', options: [ { value: '', label: 'All Countries' }, { value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }, { value: 'GB', label: 'United Kingdom' }, { value: 'AE', label: 'United Arab Emirates' }, { value: 'AU', label: 'Australia' }, { value: 'IN', label: 'India' }, { value: 'PK', label: 'Pakistan' }, ], }, { 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: 'Keywords', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, tooltip: 'Total keywords added to site wrokflow. Minimum 5 Keywords are needed for clustering.', }, { label: 'Clustered', value: 0, accentColor: 'green' as const, calculate: (data) => data.keywords.filter((k: Keyword) => k.cluster_id).length, tooltip: 'Keywords grouped into topical clusters. Clustered keywords are ready for content ideation.', }, { label: 'Unmapped', value: 0, accentColor: 'amber' as const, calculate: (data) => data.keywords.filter((k: Keyword) => !k.cluster_id).length, tooltip: 'Unclustered keywords waiting to be organized. Select keywords and use Auto-Cluster to group them.', }, { label: 'Volume', value: 0, accentColor: 'purple' as const, calculate: (data) => data.keywords.reduce((sum: number, k: Keyword) => sum + (k.volume || 0), 0), tooltip: 'Total monthly search volume across all keywords. Higher volume = more traffic potential.', }, ], // 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: 'keyword', label: 'Keyword', type: 'text', placeholder: 'Enter keyword (e.g., best massage chairs)', value: handlers.formData.keyword || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, keyword: value }), required: true, }, { key: 'volume', label: 'Search Volume', type: 'number', placeholder: 'Monthly search volume', value: handlers.formData.volume ?? '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, volume: value ? parseInt(value) : null }), required: true, min: 0, }, { key: 'difficulty', label: 'Difficulty (0-100)', type: 'number', placeholder: 'SEO difficulty score', value: handlers.formData.difficulty ?? '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, difficulty: value ? parseInt(value) : null }), required: true, min: 0, max: 100, }, { key: 'country', label: 'Country', type: 'select', value: handlers.formData.country || 'US', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, country: value }), required: true, options: [ { value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }, { value: 'GB', label: 'United Kingdom' }, { value: 'AE', label: 'United Arab Emirates' }, { value: 'AU', label: 'Australia' }, { value: 'IN', label: 'India' }, { value: 'PK', label: 'Pakistan' }, ], }, { 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 || 'new', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, status: value }), options: [ { value: 'new', label: 'New' }, { value: 'mapped', label: 'Mapped' }, ], }, ], 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 }, }; };