# 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` | `<Input>` | Text search input |
| `select` | `<SelectDropdown>` | 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 <Input ... />;
  }
  if (filter.type === 'select') {
    return <SelectDropdown ... />;
  }
  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` |
