# IGNY8 Filters Implementation **Last Updated:** January 15, 2026 **Version:** 1.0.0 > This document describes the current filter implementation across all pages that use the `TablePageTemplate` component. --- ## Architecture Overview ### Component Hierarchy ``` Page Component (e.g., Keywords.tsx) └── createPageConfig() function ├── Returns: columns, filters, formFields, headerMetrics └── Uses: handlers (state setters, filter values) └── TablePageTemplate.tsx ├── Renders filters based on FilterConfig[] ├── Manages filter visibility toggle └── Passes filterValues and onFilterChange to children ``` ### Filter Flow 1. **Page Component** defines filter state (`useState`) 2. **Config Function** creates `FilterConfig[]` with options 3. **TablePageTemplate** renders filter UI components 4. **Filter Change** → `onFilterChange` callback → update state → re-fetch data --- ## Filter Types | Type | Component | Description | |------|-----------|-------------| | `text` | `` | Text search input | | `select` | `` | Single-select dropdown | | `custom` | `customRender()` | Custom component (e.g., volume range) | | `daterange` | (planned) | Date range picker | | `range` | (planned) | Numeric range | --- ## Backend FilterSet Classes ### Planner Module (`/backend/igny8_core/modules/planner/views.py`) #### KeywordsFilter ```python class KeywordsFilter(django_filters.FilterSet): class Meta: model = Keywords fields = ['status', 'cluster_id', 'seed_keyword__country', 'seed_keyword_id', 'created_at__gte', 'created_at__lte'] ``` #### ClustersFilter ```python class ClustersFilter(django_filters.FilterSet): class Meta: model = Clusters fields = ['status', 'created_at__gte', 'created_at__lte'] ``` #### ContentIdeasFilter ```python class ContentIdeasFilter(django_filters.FilterSet): class Meta: model = ContentIdeas fields = ['status', 'keyword_cluster_id', 'content_type', 'content_structure', 'created_at__gte', 'created_at__lte'] ``` ### Writer Module (`/backend/igny8_core/modules/writer/views.py`) #### TasksFilter ```python class TasksFilter(django_filters.FilterSet): class Meta: model = Tasks fields = ['status', 'cluster_id', 'content_type', 'content_structure', 'created_at__gte', 'created_at__lte'] ``` #### ImagesFilter ```python class ImagesFilter(django_filters.FilterSet): class Meta: model = Images fields = ['task_id', 'content_id', 'image_type', 'status', 'created_at__gte', 'created_at__lte'] ``` #### ContentFilter ```python class ContentFilter(django_filters.FilterSet): class Meta: model = Content fields = ['cluster_id', 'status', 'content_type', 'content_structure', 'source', 'created_at__gte', 'created_at__lte'] ``` --- ## Page-by-Page Filter Implementation ### 1. Keywords Page (`/planner/keywords`) **Config File:** `frontend/src/config/pages/keywords.config.tsx` **Page File:** `frontend/src/pages/Planner/Keywords.tsx` | Filter Key | Type | Options | Backend Field | |------------|------|---------|---------------| | `search` | text | - | `search` (SearchFilter) | | `status` | select | `new`, `mapped` | `status` | | `country` | select | `US`, `CA`, `GB`, `AE`, `AU`, `IN`, `PK` | `seed_keyword__country` | | `difficulty` | select | `1-5` (mapped labels) | Custom in `get_queryset()` | | `cluster` | select | Dynamic (cluster list) | `cluster_id` | | `volume` | custom | Min/Max inputs | Custom `volume_min`, `volume_max` | **Special Handling:** - Difficulty filter uses 1-5 scale that maps to raw score ranges (0-10, 11-30, 31-50, 51-70, 71-100) - Volume range uses custom dropdown with min/max inputs --- ### 2. Clusters Page (`/planner/clusters`) **Config File:** `frontend/src/config/pages/clusters.config.tsx` **Page File:** `frontend/src/pages/Planner/Clusters.tsx` | Filter Key | Type | Options | Backend Field | |------------|------|---------|---------------| | `search` | text | - | `search` (SearchFilter) | | `status` | select | `new`, `mapped` | `status` | | `difficulty` | select | `1-5` (mapped labels) | Custom filtering | | `volume` | custom | Min/Max inputs | Custom `volume_min`, `volume_max` | --- ### 3. Ideas Page (`/planner/ideas`) **Config File:** `frontend/src/config/pages/ideas.config.tsx` **Page File:** `frontend/src/pages/Planner/Ideas.tsx` | Filter Key | Type | Options | Backend Field | |------------|------|---------|---------------| | `search` | text | - | `search` (SearchFilter) | | `status` | select | `new`, `queued`, `completed` | `status` | | `content_structure` | select | 14 structure types | `content_structure` | | `content_type` | select | `post`, `page`, `product`, `taxonomy` | `content_type` | | `keyword_cluster_id` | select | Dynamic (cluster list) | `keyword_cluster_id` | **Structure Options:** - Post: `article`, `guide`, `comparison`, `review`, `listicle` - Page: `landing_page`, `business_page`, `service_page`, `general`, `cluster_hub` - Product: `product_page` - Taxonomy: `category_archive`, `tag_archive`, `attribute_archive` --- ### 4. Content Page (`/writer/content`) **Config File:** `frontend/src/config/pages/content.config.tsx` **Page File:** `frontend/src/pages/Writer/Content.tsx` | Filter Key | Type | Options | Backend Field | |------------|------|---------|---------------| | `search` | text | - | `search` (SearchFilter) | | `status` | select | `draft`, `published` | `status` | | `content_type` | select | `post`, `page`, `product`, `taxonomy` | `content_type` | | `content_structure` | select | 14 structure types | `content_structure` | | `source` | select | `igny8`, `wordpress` | `source` | --- ### 5. Review Page (`/writer/review`) **Config File:** `frontend/src/config/pages/review.config.tsx` **Page File:** `frontend/src/pages/Writer/Review.tsx` | Filter Key | Type | Options | Backend Field | |------------|------|---------|---------------| | `search` | text | - | `search` (SearchFilter) | | `status` | select | `draft`, `review`, `approved`, `published` | `status` | | `site_status` | select | `not_published`, `scheduled`, `publishing`, `published`, `failed` | `site_status` | | `content_type` | select | From `CONTENT_TYPE_OPTIONS` | `content_type` | | `content_structure` | select | From `ALL_CONTENT_STRUCTURES` | `content_structure` | --- ### 6. Approved Page (`/writer/approved`) **Config File:** `frontend/src/config/pages/approved.config.tsx` **Page File:** `frontend/src/pages/Writer/Approved.tsx` | Filter Key | Type | Options | Backend Field | |------------|------|---------|---------------| | `search` | text | - | `search` (SearchFilter) | | `status` | select | `draft`, `review`, `approved`, `published` | `status` | | `site_status` | select | `not_published`, `scheduled`, `publishing`, `published`, `failed` | `site_status` | | `content_type` | select | From `CONTENT_TYPE_OPTIONS` | `content_type` | | `content_structure` | select | From `ALL_CONTENT_STRUCTURES` | `content_structure` | --- ## Shared Constants **File:** `frontend/src/config/structureMapping.ts` ```typescript export const CONTENT_TYPE_OPTIONS = [ { value: 'post', label: 'Post' }, { value: 'page', label: 'Page' }, { value: 'product', label: 'Product' }, { value: 'taxonomy', label: 'Taxonomy' }, ]; export const ALL_CONTENT_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' }, ]; ``` --- ## Difficulty Mapping **File:** `frontend/src/utils/difficulty.ts` The difficulty filter uses a 1-5 scale with human-readable labels: | Value | Label | Raw Score Range | |-------|-------|-----------------| | 1 | Very Easy | 0-10 | | 2 | Easy | 11-30 | | 3 | Medium | 31-50 | | 4 | Hard | 51-70 | | 5 | Very Hard | 71-100 | **Important:** The database stores raw SEO difficulty scores (0-100), but the UI displays and filters using the 1-5 scale. The mapping must be consistent between frontend display and backend filtering. --- ## Filter State Management Pattern Each page follows this pattern: ```tsx // 1. Define filter state const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [difficultyFilter, setDifficultyFilter] = useState(''); // 2. Pass to config function const config = createPageConfig({ searchTerm, setSearchTerm, statusFilter, setStatusFilter, // ...handlers }); // 3. Config returns filters array filters: [ { key: 'search', type: 'text', placeholder: '...' }, { key: 'status', type: 'select', options: [...] }, ] // 4. TablePageTemplate renders filters // 5. onFilterChange triggers state update // 6. useEffect with dependencies re-fetches data ``` --- ## API Query Parameters When filters are applied, the frontend constructs API calls like: ``` GET /api/v1/planner/keywords/?status=new&cluster_id=123&search=term&page=1&page_size=50 GET /api/v1/planner/clusters/?status=mapped&difficulty=3&volume_min=100 GET /api/v1/planner/ideas/?content_structure=guide&content_type=post GET /api/v1/writer/content/?status=draft&source=igny8 ``` --- ## TablePageTemplate Filter Rendering The template handles filter rendering in `renderFiltersRow()`: ```tsx {filters.map((filter) => { if (filter.type === 'text') { return ; } if (filter.type === 'select') { return ; } if (filter.type === 'custom' && filter.customRender) { return filter.customRender(); } })} ``` --- ## Current Limitations 1. **Static Options**: Filter options are hardcoded in config files, not dynamically loaded from backend 2. **No Cascading**: Changing one filter doesn't update available options in other filters 3. **Difficulty Mapping**: Backend filters by exact match, not by mapped ranges 4. **No Filter Persistence**: Filters reset on page navigation --- ## Planned Improvements 1. **Dynamic Filter Options API**: Backend endpoint to return available options based on current data 2. **Cascading Filters**: When status is selected, only show clusters/types that exist with that status 3. **URL State**: Persist filter state in URL query parameters 4. **Filter Presets**: Save commonly used filter combinations --- ## File References | Component | Path | |-----------|------| | TablePageTemplate | `frontend/src/templates/TablePageTemplate.tsx` | | Keywords Config | `frontend/src/config/pages/keywords.config.tsx` | | Clusters Config | `frontend/src/config/pages/clusters.config.tsx` | | Ideas Config | `frontend/src/config/pages/ideas.config.tsx` | | Content Config | `frontend/src/config/pages/content.config.tsx` | | Review Config | `frontend/src/config/pages/review.config.tsx` | | Approved Config | `frontend/src/config/pages/approved.config.tsx` | | Structure Mapping | `frontend/src/config/structureMapping.ts` | | Difficulty Utils | `frontend/src/utils/difficulty.ts` | | Planner Views | `backend/igny8_core/modules/planner/views.py` | | Writer Views | `backend/igny8_core/modules/writer/views.py` |