Section 3 Completed
This commit is contained in:
95
CHANGELOG.md
95
CHANGELOG.md
@@ -1,7 +1,7 @@
|
||||
# IGNY8 Change Log
|
||||
|
||||
**Current Version:** 1.1.3
|
||||
**Last Updated:** December 27, 2025
|
||||
**Current Version:** 1.1.5
|
||||
**Last Updated:** January 2, 2025
|
||||
|
||||
---
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
| Version | Date | Summary |
|
||||
|---------|------|---------|
|
||||
| 1.1.5 | Jan 2, 2025 | Section 3 WORKFLOW modules - Planner, Writer, Progress Modal fixes |
|
||||
| 1.1.4 | Dec 27, 2025 | Section 2 SETUP modules - Add Keywords, Content Settings, Sites fixes |
|
||||
| 1.1.3 | Dec 27, 2025 | Merged RULES.md into .rules |
|
||||
| 1.1.2 | Dec 27, 2025 | Module status documentation, TODOS.md |
|
||||
| 1.1.1 | Dec 27, 2025 | Simplified AI agent rules file |
|
||||
@@ -22,6 +24,95 @@
|
||||
|
||||
---
|
||||
|
||||
## v1.1.5 - January 2, 2025
|
||||
|
||||
### Section 3 WORKFLOW Modules Implementation
|
||||
|
||||
**Planner Module:**
|
||||
- **DELETED** `KeywordOpportunities.tsx` - orphaned page, Add Keywords is source of truth
|
||||
- Removed route `/planner/keyword-opportunities` from App.tsx
|
||||
- Updated Clusters table to show ideas count badge: "X ideas" (green) or "No ideas" (gray)
|
||||
- Made cluster name clickable in Ideas table - navigates to `/planner/clusters/:id`
|
||||
|
||||
**Writer Module:**
|
||||
- Fixed duplicate tags/categories display in ContentView template
|
||||
- Removed redundant tags/categories section that appeared below the main metadata
|
||||
- Tags and categories now display only once in the Topic section
|
||||
|
||||
**Progress Modals:**
|
||||
- Fixed placeholder "X" text in image prompt progress modals:
|
||||
- Changed "Mapping Content for X Image Prompts" → "Mapping content for image prompts"
|
||||
- Changed "Writing X In‑article Image Prompts" → "Writing In‑article Image Prompts"
|
||||
- Changed "Featured Image and X In‑article..." → "Image prompts ready for generation"
|
||||
- All step labels now show actual counts when available, clean fallbacks otherwise
|
||||
|
||||
### Files Changed
|
||||
- `frontend/src/App.tsx` - Removed KeywordOpportunities import/route
|
||||
- `frontend/src/config/pages/clusters.config.tsx` - Ideas count badge styling
|
||||
- `frontend/src/config/pages/ideas.config.tsx` - Clickable cluster link
|
||||
- `frontend/src/templates/ContentViewTemplate.tsx` - Removed duplicate tags/categories
|
||||
- `frontend/src/components/common/ProgressModal.tsx` - Fixed placeholder texts
|
||||
- `docs/30-FRONTEND/PAGES.md` - Removed KeywordOpportunities references, updated version
|
||||
- `docs/20-API/ENDPOINTS.md` - Added Content Settings API documentation
|
||||
|
||||
### Files Deleted
|
||||
- `frontend/src/pages/Planner/KeywordOpportunities.tsx`
|
||||
|
||||
---
|
||||
|
||||
## v1.1.4 - December 27, 2025
|
||||
|
||||
### Section 2 SETUP Modules Implementation
|
||||
|
||||
**Add Keywords Page (`/setup/add-keywords`):**
|
||||
- Added "Not Yet Added Only" filter toggle to show only keywords not in workflow
|
||||
- Added keyword count summary: "X keywords in your workflow • Y available to add"
|
||||
- Added "Next: Plan Your Content →" CTA button (appears after adding keywords)
|
||||
- Added "Keyword Research coming soon!" teaser text
|
||||
- Sector requirement tooltip already existed - no changes needed
|
||||
|
||||
**Content Settings Page (`/account/content-settings`):**
|
||||
- **NEW Backend API**: Created `/v1/system/settings/content/<pk>/` endpoints
|
||||
- `GET /v1/system/settings/content/content_generation/` - Retrieve settings
|
||||
- `POST /v1/system/settings/content/content_generation/save/` - Save settings
|
||||
- `GET /v1/system/settings/content/publishing/` - Retrieve settings
|
||||
- `POST /v1/system/settings/content/publishing/save/` - Save settings
|
||||
- Content Generation tab now persists: append_to_prompt, default_tone, default_length
|
||||
- Publishing tab now persists: auto_publish_enabled, auto_sync_enabled
|
||||
- Fixed false "saved" confirmation - now actually saves to backend
|
||||
|
||||
**Sites Module:**
|
||||
- **NEW Component**: Created `SiteSetupChecklist` component showing setup progress
|
||||
- Displays checklist: Site created, Industry/Sectors, WordPress integration, Keywords
|
||||
- Progress bar with percentage
|
||||
- "Complete Setup" button linking to first incomplete item
|
||||
- "Ready to create content!" message when all complete
|
||||
- Updated Site Dashboard to use SiteSetupChecklist instead of mock stats
|
||||
- Removed mock statistics that showed all zeros
|
||||
|
||||
**Cleanup:**
|
||||
- Deleted `pages/Sites/Manage.tsx` (redundant duplicate of List.tsx)
|
||||
- Removed empty `pages/Sites/Builder/` folder structure
|
||||
- Removed `/sites/manage` route from App.tsx
|
||||
- Removed SiteManage lazy import from App.tsx
|
||||
|
||||
### Files Changed
|
||||
- `frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx`
|
||||
- `frontend/src/pages/account/ContentSettingsPage.tsx`
|
||||
- `frontend/src/pages/Sites/Dashboard.tsx`
|
||||
- `frontend/src/App.tsx`
|
||||
- `backend/igny8_core/modules/system/settings_views.py`
|
||||
- `backend/igny8_core/modules/system/urls.py`
|
||||
|
||||
### Files Created
|
||||
- `frontend/src/components/sites/SiteSetupChecklist.tsx`
|
||||
|
||||
### Files Deleted
|
||||
- `frontend/src/pages/Sites/Manage.tsx`
|
||||
- `frontend/src/pages/Sites/Builder/` (empty directory)
|
||||
|
||||
---
|
||||
|
||||
## v1.1.3 - December 27, 2025
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -135,6 +135,8 @@ All endpoints require authentication unless noted.
|
||||
| GET | `/settings/integrations/image_generation/` | Get image settings | Current config |
|
||||
| PUT | `/settings/integrations/image_generation/` | Save image settings | Update config |
|
||||
| POST | `/settings/integrations/test/` | Test connection | Verify API keys |
|
||||
| GET | `/settings/content/{key}/` | `ContentSettingsViewSet.retrieve` | Get content settings |
|
||||
| POST | `/settings/content/{key}/save/` | `ContentSettingsViewSet.save_settings` | Save content settings |
|
||||
| GET | `/prompts/` | List prompts | All prompts |
|
||||
| GET | `/prompts/{type}/` | Get prompt | Specific prompt |
|
||||
| PUT | `/prompts/{type}/` | Save prompt | Update prompt |
|
||||
@@ -143,6 +145,10 @@ All endpoints require authentication unless noted.
|
||||
| PUT | `/modules/` | Save modules | Update enabled |
|
||||
| GET | `/health/` | Health check | System status |
|
||||
|
||||
**Content Settings Keys:**
|
||||
- `content_generation` - AI writing settings (default_article_length, default_tone, include_faq, enable_internal_linking, etc.)
|
||||
- `publishing` - Publishing defaults (default_publish_status, auto_schedule, schedule_frequency, schedule_times)
|
||||
|
||||
---
|
||||
|
||||
## Automation Endpoints (`/api/v1/automation/`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Frontend Pages & Routes
|
||||
|
||||
**Last Verified:** December 25, 2025
|
||||
**Version:** 1.1.0
|
||||
**Last Verified:** January 2, 2025
|
||||
**Version:** 1.1.4
|
||||
**Framework:** React 19 + TypeScript + React Router 6 + Vite
|
||||
|
||||
---
|
||||
@@ -41,7 +41,14 @@ Routes defined in `/frontend/src/App.tsx`:
|
||||
|
||||
| Route | File | Description |
|
||||
|-------|------|-------------|
|
||||
| `/setup/add-keywords` | `Setup/AddKeywords.tsx` | Browse/add seed keywords from global database |
|
||||
| `/setup/add-keywords` | `Setup/IndustriesSectorsKeywords.tsx` | Browse/add seed keywords from global database |
|
||||
|
||||
**Features:**
|
||||
- Industry → Sector → Keywords browse hierarchy
|
||||
- "Show not-added only" filter toggle
|
||||
- Real-time keyword count summary (added/total)
|
||||
- "Next: Plan Your Content" CTA button
|
||||
- "Keyword Research" coming soon teaser
|
||||
|
||||
### Content Settings
|
||||
|
||||
@@ -49,14 +56,18 @@ Routes defined in `/frontend/src/App.tsx`:
|
||||
|-------|------|-------------|
|
||||
| `/account/content-settings` | `account/ContentSettingsPage.tsx` | 3 tabs: Content Generation, Publishing, Image Settings |
|
||||
|
||||
**API Endpoints:** Settings persisted via `/api/v1/system/settings/content/{key}/`
|
||||
|
||||
### Sites
|
||||
|
||||
| Route | File | Description |
|
||||
|-------|------|-------------|
|
||||
| `/sites` | `Sites/List.tsx` | Site listing with filters |
|
||||
| `/sites/:id/dashboard` | `Sites/SiteDashboard.tsx` | Individual site overview |
|
||||
| `/sites/:id/settings` | `Sites/SiteSettings.tsx` | Site settings (General, Integrations, Content Types) |
|
||||
| `/sites/:id/content` | `Sites/SiteContent.tsx` | Site content management |
|
||||
| `/sites` | `Sites/List.tsx` | Site listing with setup checklist per site |
|
||||
| `/sites/:id/dashboard` | `Sites/Dashboard.tsx` | Site overview with setup checklist |
|
||||
| `/sites/:id/settings` | `Sites/Settings.tsx` | Site settings (General, Integrations, Content Types) |
|
||||
|
||||
**Components:**
|
||||
- `SiteSetupChecklist` - Shows setup progress (site created, industry/sectors, WordPress, keywords)
|
||||
|
||||
### Thinker (Admin Only)
|
||||
|
||||
@@ -80,7 +91,6 @@ Routes defined in `/frontend/src/App.tsx`:
|
||||
| `/planner/clusters` | `Planner/Clusters.tsx` | Cluster listing, AI clustering | Clusters |
|
||||
| `/planner/clusters/:id` | `Planner/ClusterView.tsx` | Individual cluster view | - |
|
||||
| `/planner/ideas` | `Planner/Ideas.tsx` | Content ideas, queue to writer | Ideas |
|
||||
| `/planner/keyword-opportunities` | `Planner/KeywordOpportunities.tsx` | Seed keyword discovery (hidden) | - |
|
||||
|
||||
### Writer
|
||||
|
||||
@@ -228,18 +238,15 @@ frontend/src/pages/
|
||||
│ ├── Keywords.tsx
|
||||
│ ├── Clusters.tsx
|
||||
│ ├── ClusterView.tsx
|
||||
│ ├── Ideas.tsx
|
||||
│ └── KeywordOpportunities.tsx # Not in nav
|
||||
│ └── Ideas.tsx
|
||||
├── Settings/
|
||||
│ └── IntegrationPage.tsx # AI Models (admin)
|
||||
├── Setup/
|
||||
│ └── AddKeywords.tsx
|
||||
│ └── IndustriesSectorsKeywords.tsx # Add Keywords page
|
||||
├── Sites/
|
||||
│ ├── List.tsx
|
||||
│ ├── SiteDashboard.tsx
|
||||
│ ├── SiteSettings.tsx
|
||||
│ ├── SiteContent.tsx
|
||||
│ └── Manage.tsx # Possibly redundant
|
||||
│ ├── List.tsx # Site listing
|
||||
│ ├── Dashboard.tsx # Site overview + checklist
|
||||
│ └── Settings.tsx # Site configuration
|
||||
├── Thinker/
|
||||
│ ├── Prompts.tsx
|
||||
│ ├── AuthorProfiles.tsx
|
||||
@@ -303,10 +310,9 @@ Dashboard
|
||||
|
||||
## Known Issues (from Audit)
|
||||
|
||||
1. **KeywordOpportunities** not accessible from navigation
|
||||
2. **Linker/Optimizer Dashboards** exist but not exposed
|
||||
3. **Help sub-pages** are placeholders
|
||||
4. **ContentView** is read-only (no editing capability)
|
||||
5. Legacy redirects may cause confusion
|
||||
1. **Linker/Optimizer Dashboards** exist but not exposed in navigation
|
||||
2. **Help sub-pages** are placeholders
|
||||
3. **ContentView** is read-only (no editing capability)
|
||||
4. Legacy redirects may cause confusion
|
||||
|
||||
See `/PRE-LAUNCH-AUDIT.md` for complete issue list.
|
||||
|
||||
@@ -25,7 +25,6 @@ const Keywords = lazy(() => import("./pages/Planner/Keywords"));
|
||||
const Clusters = lazy(() => import("./pages/Planner/Clusters"));
|
||||
const ClusterDetail = lazy(() => import("./pages/Planner/ClusterDetail"));
|
||||
const Ideas = lazy(() => import("./pages/Planner/Ideas"));
|
||||
const KeywordOpportunities = lazy(() => import("./pages/Planner/KeywordOpportunities"));
|
||||
|
||||
// Writer Module - Lazy loaded
|
||||
const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard"));
|
||||
@@ -203,7 +202,6 @@ export default function App() {
|
||||
|
||||
{/* Reference Data */}
|
||||
<Route path="/reference/seed-keywords" element={<SeedKeywords />} />
|
||||
<Route path="/planner/keyword-opportunities" element={<KeywordOpportunities />} />
|
||||
<Route path="/reference/industries" element={<ReferenceIndustries />} />
|
||||
|
||||
{/* Setup Pages */}
|
||||
|
||||
@@ -127,7 +127,7 @@ const getSuccessMessage = (functionId?: string, title?: string, stepLogs?: any[]
|
||||
}
|
||||
|
||||
// Default message
|
||||
return 'Featured Image and X In‑article Image Prompts ready for image generation';
|
||||
return 'Image prompts ready for generation';
|
||||
}
|
||||
return 'Task completed successfully.';
|
||||
};
|
||||
@@ -180,9 +180,9 @@ const getStepsForFunction = (functionId?: string, title?: string): Array<{phase:
|
||||
// Image prompt generation
|
||||
return [
|
||||
{ phase: 'INIT', label: 'Checking content and image slots' },
|
||||
{ phase: 'PREP', label: 'Mapping Content for X Image Prompts' },
|
||||
{ phase: 'PREP', label: 'Mapping content for image prompts' },
|
||||
{ phase: 'AI_CALL', label: 'Writing Featured Image Prompts' },
|
||||
{ phase: 'PARSE', label: 'Writing X In‑article Image Prompts' },
|
||||
{ phase: 'PARSE', label: 'Writing In‑article Image Prompts' },
|
||||
{ phase: 'SAVE', label: 'Assigning Prompts to Dedicated Slots' },
|
||||
];
|
||||
}
|
||||
@@ -457,7 +457,7 @@ export default function ProgressModal({
|
||||
return `Mapping Content for ${match[1]} Image Prompts`;
|
||||
}
|
||||
}
|
||||
return 'Mapping Content for X Image Prompts';
|
||||
return 'Mapping content for image prompts';
|
||||
} else if (stepPhase === 'AI_CALL') {
|
||||
// For AI_CALL: Show "Writing Featured Image Prompts"
|
||||
return 'Writing Featured Image Prompts';
|
||||
@@ -475,7 +475,7 @@ export default function ProgressModal({
|
||||
return `Writing ${match[1]} In‑article Image Prompts`;
|
||||
}
|
||||
}
|
||||
return 'Writing X In‑article Image Prompts';
|
||||
return 'Writing In‑article Image Prompts';
|
||||
} else if (stepPhase === 'SAVE') {
|
||||
// For SAVE: Extract prompt count from message
|
||||
const promptCount = extractCount(/(\d+)\s+Prompts/i) || extractCount(/(\d+)\s+prompt/i);
|
||||
|
||||
@@ -137,7 +137,17 @@ export const createClustersPageConfig = (
|
||||
sortable: false, // Backend doesn't support sorting by ideas_count
|
||||
sortField: 'ideas_count',
|
||||
width: '120px',
|
||||
render: (value: number) => value.toLocaleString(),
|
||||
render: (value: number) => (
|
||||
<Badge
|
||||
color={value > 0 ? 'success' : 'light'}
|
||||
size="xs"
|
||||
variant="soft"
|
||||
>
|
||||
<span className="text-[11px] font-normal">
|
||||
{value > 0 ? `${value.toLocaleString()} ideas` : 'No ideas'}
|
||||
</span>
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'volume',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
titleColumn,
|
||||
sectorColumn,
|
||||
@@ -164,7 +165,19 @@ export const createIdeasPageConfig = (
|
||||
sortable: false, // Backend doesn't support sorting by keyword_cluster_id
|
||||
sortField: 'keyword_cluster_id',
|
||||
width: '200px',
|
||||
render: (_value: string, row: ContentIdea) => row.keyword_cluster_name || '-',
|
||||
render: (_value: string, row: ContentIdea) => {
|
||||
if (row.keyword_cluster_id && row.keyword_cluster_name) {
|
||||
return (
|
||||
<Link
|
||||
to={`/planner/clusters/${row.keyword_cluster_id}`}
|
||||
className="text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 hover:underline"
|
||||
>
|
||||
{row.keyword_cluster_name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
...statusColumn,
|
||||
|
||||
@@ -1,680 +0,0 @@
|
||||
/**
|
||||
* Keyword Opportunities Page
|
||||
* Shows available SeedKeywords for the active site/sectors
|
||||
* Allows users to add keywords to their workflow
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchSeedKeywords,
|
||||
SeedKeyword,
|
||||
SeedKeywordResponse,
|
||||
addSeedKeywordsToWorkflow,
|
||||
} from '../../services/api';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { getDifficultyLabelFromNumber, getDifficultyRange, getDifficultyNumber } from '../../utils/difficulty';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { formatRelativeDate } from '../../utils/date';
|
||||
import { BoltIcon, PlusIcon } from '../../icons';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
|
||||
export default function KeywordOpportunities() {
|
||||
const toast = useToast();
|
||||
const { activeSite } = useSiteStore();
|
||||
const { activeSector, loadSectorsForSite } = useSectorStore();
|
||||
const { pageSize } = usePageSizeStore();
|
||||
|
||||
// Data state
|
||||
const [seedKeywords, setSeedKeywords] = useState<(SeedKeyword & { isAdded?: boolean })[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
// Track recently added keywords to preserve their state during reload
|
||||
const recentlyAddedRef = useRef<Set<number>>(new Set());
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// Sorting state
|
||||
const [sortBy, setSortBy] = useState<string>('keyword');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
// Filter state
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [countryFilter, setCountryFilter] = useState('');
|
||||
const [difficultyFilter, setDifficultyFilter] = useState('');
|
||||
const [volumeMin, setVolumeMin] = useState<number | ''>('');
|
||||
const [volumeMax, setVolumeMax] = useState<number | ''>('');
|
||||
|
||||
// Load sectors for active site
|
||||
useEffect(() => {
|
||||
if (activeSite?.id) {
|
||||
loadSectorsForSite(activeSite.id);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeSite?.id]); // loadSectorsForSite is stable from Zustand store, no need to include it
|
||||
|
||||
// Load seed keywords
|
||||
const loadSeedKeywords = useCallback(async () => {
|
||||
if (!activeSite || !activeSite.industry) {
|
||||
setSeedKeywords([]);
|
||||
setTotalCount(0);
|
||||
setTotalPages(1);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setShowContent(false);
|
||||
|
||||
try {
|
||||
// Get already-attached keywords across ALL sectors for this site
|
||||
let attachedSeedKeywordIds = new Set<number>();
|
||||
try {
|
||||
const { fetchKeywords, fetchSiteSectors } = await import('../../services/api');
|
||||
// Get all sectors for the site
|
||||
const sectors = await fetchSiteSectors(activeSite.id);
|
||||
|
||||
// Check keywords in all sectors
|
||||
for (const sector of sectors) {
|
||||
try {
|
||||
const keywordsData = await fetchKeywords({
|
||||
site_id: activeSite.id,
|
||||
sector_id: sector.id,
|
||||
page_size: 1000, // Get all to check which are attached
|
||||
});
|
||||
(keywordsData.results || []).forEach((k: any) => {
|
||||
// seed_keyword_id is write_only in serializer, so use seed_keyword.id instead
|
||||
const seedKeywordId = k.seed_keyword_id || (k.seed_keyword && k.seed_keyword.id);
|
||||
if (seedKeywordId) {
|
||||
attachedSeedKeywordIds.add(Number(seedKeywordId));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// If keywords fetch fails for a sector, continue with others
|
||||
console.warn(`Could not fetch attached keywords for sector ${sector.id}:`, err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// If sectors fetch fails, continue without filtering
|
||||
console.warn('Could not fetch sectors or attached keywords:', err);
|
||||
}
|
||||
|
||||
// Build filters - fetch ALL results by paginating through all pages
|
||||
const baseFilters: any = {
|
||||
industry: activeSite.industry,
|
||||
page_size: 1000, // Use reasonable page size (API might have max limit)
|
||||
};
|
||||
|
||||
// Add sector filter if active sector is selected
|
||||
// IMPORTANT: Filter by industry_sector (IndustrySector ID) which is what SeedKeyword.sector references
|
||||
if (activeSector && activeSector.industry_sector) {
|
||||
baseFilters.sector = activeSector.industry_sector;
|
||||
}
|
||||
|
||||
if (searchTerm) baseFilters.search = searchTerm;
|
||||
if (countryFilter) baseFilters.country = countryFilter;
|
||||
|
||||
// Fetch ALL pages to get complete dataset
|
||||
let allResults: SeedKeyword[] = [];
|
||||
let currentPageNum = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const filters = { ...baseFilters, page: currentPageNum };
|
||||
const data: SeedKeywordResponse = await fetchSeedKeywords(filters);
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
allResults = [...allResults, ...data.results];
|
||||
}
|
||||
|
||||
// Check if there are more pages
|
||||
hasMore = data.next !== null && data.next !== undefined;
|
||||
currentPageNum++;
|
||||
|
||||
// Safety limit to prevent infinite loops
|
||||
if (currentPageNum > 100) {
|
||||
console.warn('Reached maximum page limit (100) while fetching seed keywords');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark already-attached keywords instead of filtering them out
|
||||
// Also check recentlyAddedRef to preserve state for keywords just added
|
||||
let filteredResults = allResults.map(sk => {
|
||||
const isAdded = attachedSeedKeywordIds.has(Number(sk.id)) || recentlyAddedRef.current.has(Number(sk.id));
|
||||
return {
|
||||
...sk,
|
||||
isAdded: Boolean(isAdded) // Explicitly convert to boolean true/false
|
||||
};
|
||||
});
|
||||
|
||||
if (difficultyFilter) {
|
||||
const difficultyNum = parseInt(difficultyFilter);
|
||||
const label = getDifficultyLabelFromNumber(difficultyNum);
|
||||
if (label !== null) {
|
||||
const range = getDifficultyRange(label);
|
||||
if (range) {
|
||||
filteredResults = filteredResults.filter(
|
||||
sk => sk.difficulty >= range.min && sk.difficulty <= range.max
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (volumeMin !== '' && volumeMin !== null && volumeMin !== undefined) {
|
||||
filteredResults = filteredResults.filter(sk => sk.volume >= Number(volumeMin));
|
||||
}
|
||||
if (volumeMax !== '' && volumeMax !== null && volumeMax !== undefined) {
|
||||
filteredResults = filteredResults.filter(sk => sk.volume <= Number(volumeMax));
|
||||
}
|
||||
|
||||
// Apply client-side sorting
|
||||
if (sortBy) {
|
||||
filteredResults.sort((a, b) => {
|
||||
let aVal: any;
|
||||
let bVal: any;
|
||||
|
||||
if (sortBy === 'keyword') {
|
||||
aVal = a.keyword.toLowerCase();
|
||||
bVal = b.keyword.toLowerCase();
|
||||
} else if (sortBy === 'volume') {
|
||||
aVal = a.volume;
|
||||
bVal = b.volume;
|
||||
} else if (sortBy === 'difficulty') {
|
||||
aVal = a.difficulty;
|
||||
bVal = b.difficulty;
|
||||
} else if (sortBy === 'intent') {
|
||||
aVal = a.intent.toLowerCase();
|
||||
bVal = b.intent.toLowerCase();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total count and pages from filtered results
|
||||
const totalFiltered = filteredResults.length;
|
||||
const pageSizeNum = pageSize || 10;
|
||||
|
||||
// Apply client-side pagination
|
||||
const startIndex = (currentPage - 1) * pageSizeNum;
|
||||
const endIndex = startIndex + pageSizeNum;
|
||||
const paginatedResults = filteredResults.slice(startIndex, endIndex);
|
||||
|
||||
|
||||
setSeedKeywords(paginatedResults);
|
||||
setTotalCount(totalFiltered);
|
||||
setTotalPages(Math.ceil(totalFiltered / pageSizeNum));
|
||||
|
||||
setShowContent(true);
|
||||
} catch (error: any) {
|
||||
console.error('Error loading seed keywords:', error);
|
||||
toast.error(`Failed to load keyword opportunities: ${error.message}`);
|
||||
setSeedKeywords([]);
|
||||
setTotalCount(0);
|
||||
setTotalPages(1);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection]);
|
||||
|
||||
// Load data on mount and when filters change (excluding search - handled separately)
|
||||
useEffect(() => {
|
||||
loadSeedKeywords();
|
||||
}, [loadSeedKeywords]);
|
||||
|
||||
// Debounced search - reset to page 1 when search term changes
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setCurrentPage(1);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]); // Only depend on searchTerm
|
||||
|
||||
// Handle pageSize changes - reload data when pageSize changes
|
||||
// Note: loadSeedKeywords will be recreated when pageSize changes (it's in its dependencies)
|
||||
// The effect that depends on loadSeedKeywords will handle the reload
|
||||
// We just need to reset to page 1
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [pageSize]); // Only depend on pageSize
|
||||
|
||||
// Handle sorting
|
||||
const handleSort = (field: string, direction: 'asc' | 'desc') => {
|
||||
setSortBy(field || 'keyword');
|
||||
setSortDirection(direction);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Handle adding keywords to workflow
|
||||
const handleAddToWorkflow = useCallback(async (seedKeywordIds: number[]) => {
|
||||
if (!activeSite) {
|
||||
toast.error('Please select an active site first');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get sector to use - use activeSector if available, otherwise get first available sector
|
||||
let sectorToUse = activeSector;
|
||||
if (!sectorToUse) {
|
||||
try {
|
||||
const { fetchSiteSectors } = await import('../../services/api');
|
||||
const sectors = await fetchSiteSectors(activeSite.id);
|
||||
if (sectors.length === 0) {
|
||||
toast.error('No sectors available for this site. Please create a sector first.');
|
||||
return;
|
||||
}
|
||||
sectorToUse = {
|
||||
id: sectors[0].id,
|
||||
name: sectors[0].name,
|
||||
slug: sectors[0].slug,
|
||||
site_id: activeSite.id,
|
||||
is_active: sectors[0].is_active !== false,
|
||||
industry_sector: sectors[0].industry_sector || null,
|
||||
};
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to get sectors: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await addSeedKeywordsToWorkflow(
|
||||
seedKeywordIds,
|
||||
activeSite.id,
|
||||
sectorToUse.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
// Show success message with created count
|
||||
if (result.created > 0) {
|
||||
toast.success(`Successfully added ${result.created} keyword(s) to workflow`);
|
||||
}
|
||||
|
||||
// Show skipped count if any
|
||||
if (result.skipped > 0) {
|
||||
toast.warning(`${result.skipped} keyword(s) were skipped (already exist or validation failed)`);
|
||||
}
|
||||
|
||||
// Show detailed errors if any
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
result.errors.forEach((error: string) => {
|
||||
toast.error(error, { duration: 8000 });
|
||||
});
|
||||
}
|
||||
|
||||
// Only track and mark as added if actually created
|
||||
if (result.created > 0) {
|
||||
// Track these as recently added to preserve state during reload
|
||||
seedKeywordIds.forEach(id => {
|
||||
recentlyAddedRef.current.add(id);
|
||||
});
|
||||
|
||||
// Immediately update state to mark keywords as added - this gives instant feedback
|
||||
setSeedKeywords(prevKeywords =>
|
||||
prevKeywords.map(kw =>
|
||||
seedKeywordIds.includes(kw.id)
|
||||
? { ...kw, isAdded: true }
|
||||
: kw
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
setSelectedIds([]);
|
||||
|
||||
// Don't reload immediately - the state is already updated
|
||||
// The recentlyAddedRef will ensure they stay marked as added
|
||||
// Only reload if user changes filters/pagination
|
||||
} else {
|
||||
toast.error(`Failed to add keywords: ${result.errors?.join(', ') || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to add keywords: ${error.message}`);
|
||||
}
|
||||
}, [activeSite, activeSector, toast]);
|
||||
|
||||
// Handle bulk add selected - filter out already added keywords
|
||||
const handleBulkAddSelected = useCallback(async (ids: string[]) => {
|
||||
if (ids.length === 0) {
|
||||
toast.error('Please select at least one keyword');
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out already added keywords
|
||||
const availableIds = ids.filter(id => {
|
||||
const keyword = seedKeywords.find(sk => String(sk.id) === id);
|
||||
return keyword && !keyword.isAdded;
|
||||
});
|
||||
|
||||
if (availableIds.length === 0) {
|
||||
toast.error('All selected keywords are already added to workflow');
|
||||
return;
|
||||
}
|
||||
|
||||
if (availableIds.length < ids.length) {
|
||||
toast.info(`${ids.length - availableIds.length} keyword(s) were already added and were skipped`);
|
||||
}
|
||||
|
||||
const seedKeywordIds = availableIds.map(id => parseInt(id));
|
||||
await handleAddToWorkflow(seedKeywordIds);
|
||||
}, [handleAddToWorkflow, toast, seedKeywords]);
|
||||
|
||||
// Handle add all - fetch all keywords for site/sectors, not just current page
|
||||
const handleAddAll = useCallback(async () => {
|
||||
if (!activeSite || !activeSite.industry) {
|
||||
toast.error('Please select an active site first');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch ALL seed keywords for the site/sectors (no pagination)
|
||||
const filters: any = {
|
||||
industry: activeSite.industry,
|
||||
page_size: 1000, // Large page size to get all
|
||||
};
|
||||
|
||||
if (activeSector?.industry_sector) {
|
||||
filters.sector = activeSector.industry_sector;
|
||||
}
|
||||
|
||||
const data: SeedKeywordResponse = await fetchSeedKeywords(filters);
|
||||
const allSeedKeywords = data.results || [];
|
||||
|
||||
if (allSeedKeywords.length === 0) {
|
||||
toast.error('No keywords available to add');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get already-added keywords to filter them out
|
||||
const { fetchKeywords, fetchSiteSectors } = await import('../../services/api');
|
||||
const sectors = await fetchSiteSectors(activeSite.id);
|
||||
let attachedSeedKeywordIds = new Set<number>();
|
||||
|
||||
for (const sector of sectors) {
|
||||
try {
|
||||
const keywordsData = await fetchKeywords({
|
||||
site_id: activeSite.id,
|
||||
sector_id: sector.id,
|
||||
page_size: 1000,
|
||||
});
|
||||
(keywordsData.results || []).forEach((k: any) => {
|
||||
// seed_keyword_id is write_only in serializer, so use seed_keyword.id instead
|
||||
const seedKeywordId = k.seed_keyword_id || (k.seed_keyword && k.seed_keyword.id);
|
||||
if (seedKeywordId) {
|
||||
attachedSeedKeywordIds.add(Number(seedKeywordId));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(`Could not fetch attached keywords for sector ${sector.id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out already added keywords
|
||||
const availableKeywords = allSeedKeywords.filter(sk => !attachedSeedKeywordIds.has(sk.id));
|
||||
|
||||
if (availableKeywords.length === 0) {
|
||||
toast.error('All keywords are already added to workflow');
|
||||
return;
|
||||
}
|
||||
|
||||
if (availableKeywords.length < allSeedKeywords.length) {
|
||||
toast.info(`${allSeedKeywords.length - availableKeywords.length} keyword(s) were already added and were skipped`);
|
||||
}
|
||||
|
||||
const seedKeywordIds = availableKeywords.map(sk => sk.id);
|
||||
await handleAddToWorkflow(seedKeywordIds);
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load all keywords: ${error.message}`);
|
||||
}
|
||||
}, [activeSite, activeSector, handleAddToWorkflow, toast]);
|
||||
|
||||
// Page config
|
||||
const pageConfig = useMemo(() => {
|
||||
const showSectorColumn = !activeSector; // Show when viewing all sectors
|
||||
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
key: 'keyword',
|
||||
label: 'Keyword',
|
||||
sortable: true,
|
||||
sortField: 'keyword',
|
||||
},
|
||||
...(showSectorColumn ? [{
|
||||
key: 'sector_name',
|
||||
label: 'Sector',
|
||||
sortable: false,
|
||||
render: (_value: string, row: SeedKeyword) => (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{row.sector_name || '-'}
|
||||
</Badge>
|
||||
),
|
||||
}] : []),
|
||||
{
|
||||
key: 'volume',
|
||||
label: 'Volume',
|
||||
sortable: true,
|
||||
sortField: 'volume',
|
||||
render: (value: number) => value.toLocaleString(),
|
||||
},
|
||||
{
|
||||
key: 'difficulty',
|
||||
label: 'Difficulty',
|
||||
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' ? (
|
||||
<Badge
|
||||
color={difficultyBadgeColor}
|
||||
variant={difficultyBadgeVariant}
|
||||
size="sm"
|
||||
>
|
||||
{difficultyNum}
|
||||
</Badge>
|
||||
) : (
|
||||
difficultyNum
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'country',
|
||||
label: 'Country',
|
||||
sortable: true,
|
||||
sortField: 'country',
|
||||
render: (value: string) => {
|
||||
const countryNames: Record<string, string> = {
|
||||
'US': 'United States',
|
||||
'CA': 'Canada',
|
||||
'GB': 'United Kingdom',
|
||||
'AE': 'United Arab Emirates',
|
||||
'AU': 'Australia',
|
||||
'IN': 'India',
|
||||
'PK': 'Pakistan',
|
||||
};
|
||||
return (
|
||||
<Badge
|
||||
color="info"
|
||||
size="sm"
|
||||
variant="light"
|
||||
>
|
||||
{value || '-'}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'search',
|
||||
label: 'Search',
|
||||
type: 'text',
|
||||
placeholder: 'Search keywords...',
|
||||
},
|
||||
{
|
||||
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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
bulkActions: !activeSector ? [] : [
|
||||
{
|
||||
key: 'add_selected_to_workflow',
|
||||
label: 'Add Selected Keywords',
|
||||
variant: 'primary' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [activeSector]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Find & Add Keywords to Your Site"
|
||||
badge={{ icon: <BoltIcon />, color: 'orange' }}
|
||||
/>
|
||||
|
||||
{/* Show info banner when no sector is selected */}
|
||||
{!activeSector && activeSite && (
|
||||
<div className="mx-6 mt-6 mb-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-200">
|
||||
Choose a Topic Area First
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||
Pick a topic area first, then add keywords - You need to choose what you're writing about before adding search terms to target
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={seedKeywords}
|
||||
loading={loading}
|
||||
showContent={showContent}
|
||||
filters={pageConfig.filters}
|
||||
filterValues={{
|
||||
search: searchTerm,
|
||||
country: countryFilter,
|
||||
difficulty: difficultyFilter,
|
||||
}}
|
||||
onFilterChange={(key, value) => {
|
||||
const stringValue = value === null || value === undefined ? '' : String(value);
|
||||
|
||||
if (key === 'search') {
|
||||
setSearchTerm(stringValue);
|
||||
} else if (key === 'country') {
|
||||
setCountryFilter(stringValue);
|
||||
setCurrentPage(1);
|
||||
} else if (key === 'difficulty') {
|
||||
setDifficultyFilter(stringValue);
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}}
|
||||
onRowAction={async (actionKey: string, row: SeedKeyword & { isAdded?: boolean }) => {
|
||||
if (actionKey === 'add_to_workflow') {
|
||||
// Check if sector is selected
|
||||
if (!activeSector) {
|
||||
toast.error('Please select a sector first');
|
||||
return;
|
||||
}
|
||||
// Don't allow adding already-added keywords
|
||||
if (row.isAdded) {
|
||||
toast.info('This keyword is already added to workflow');
|
||||
return;
|
||||
}
|
||||
await handleAddToWorkflow([row.id]);
|
||||
}
|
||||
}}
|
||||
bulkActions={pageConfig.bulkActions}
|
||||
onBulkAction={async (actionKey: string, ids: string[]) => {
|
||||
if (actionKey === 'add_selected_to_workflow') {
|
||||
if (!activeSector) {
|
||||
toast.error('Please select a sector first');
|
||||
return;
|
||||
}
|
||||
await handleBulkAddSelected(ids);
|
||||
}
|
||||
}}
|
||||
onCreate={activeSector ? handleAddAll : undefined}
|
||||
createLabel="Add All to Workflow"
|
||||
onCreateIcon={<PlusIcon />}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalCount,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
sorting={{
|
||||
sortBy,
|
||||
sortDirection,
|
||||
onSort: handleSort,
|
||||
}}
|
||||
selection={{
|
||||
selectedIds,
|
||||
onSelectionChange: setSelectedIds,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -998,48 +998,6 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags and Categories from taxonomy_terms_data */}
|
||||
{content.taxonomy_terms_data && content.taxonomy_terms_data.length > 0 ? (
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{content.taxonomy_terms_data.filter(term => term.taxonomy_type === 'tag').length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<TagIcon className="w-4 h-4 text-gray-400" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{content.taxonomy_terms_data
|
||||
.filter(term => term.taxonomy_type === 'tag')
|
||||
.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="px-3 py-1 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300 rounded-full text-xs font-medium"
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{content.taxonomy_terms_data.filter(term => term.taxonomy_type === 'category').length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Categories:</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{content.taxonomy_terms_data
|
||||
.filter(term => term.taxonomy_type === 'category')
|
||||
.map((category) => (
|
||||
<span
|
||||
key={category.id}
|
||||
className="px-3 py-1 bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 rounded-full text-xs font-medium"
|
||||
>
|
||||
{category.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - Conditional based on status */}
|
||||
|
||||
@@ -240,3 +240,50 @@ Site Setup Progress
|
||||
| Cleanup | Remove Site Builder and content fetching APIs/models |
|
||||
|
||||
---
|
||||
|
||||
Status after implementation
|
||||
|
||||
|
||||
## Summary of Section 2 Implementation
|
||||
|
||||
### 2.1 Add Keywords (IndustriesSectorsKeywords.tsx)
|
||||
- ✅ Added `showNotAddedOnly` filter state with "Not Yet Added Only" filter option
|
||||
- ✅ Added `addedCount` and `availableCount` state variables for keyword count tracking
|
||||
- ✅ Added keyword count summary showing "X keywords in your workflow • Y available to add"
|
||||
- ✅ Added "Next: Plan Your Content →" CTA button that appears when keywords are added
|
||||
- ✅ Added "Looking for more keywords? Keyword Research coming soon!" teaser text
|
||||
- ✅ Sector requirement tooltip already existed - no changes needed
|
||||
- ✅ No visible import buttons to remove (dead code existed but was not exposed)
|
||||
|
||||
### 2.2 Content Settings (ContentSettingsPage.tsx)
|
||||
- ✅ Created new backend API endpoint `/v1/system/settings/content/<pk>/` for content_generation and publishing
|
||||
- ✅ Added `ContentSettingsViewSet` to backend with retrieve/update/save actions
|
||||
- ✅ Updated frontend to load content_generation and publishing settings from API
|
||||
- ✅ Updated frontend to save content_generation settings (append_to_prompt, default_tone, default_length)
|
||||
- ✅ Updated frontend to save publishing settings (auto_publish_enabled, auto_sync_enabled)
|
||||
- ✅ Removed TODO comments - settings now actually persist
|
||||
|
||||
### 2.3 Sites
|
||||
- ✅ Created `SiteSetupChecklist` component showing setup progress with checklist
|
||||
- ✅ Updated Dashboard.tsx to use the new checklist component
|
||||
- ✅ Removed mock stats from Dashboard (were showing all zeros)
|
||||
- ✅ Deleted Manage.tsx (redundant duplicate of List.tsx)
|
||||
- ✅ Removed empty `Builder/` folder structure
|
||||
- ✅ Removed routes to deleted pages in App.tsx
|
||||
- ✅ Site Settings already defaults to "general" tab - no changes needed
|
||||
|
||||
### Files Modified
|
||||
1. IndustriesSectorsKeywords.tsx
|
||||
2. ContentSettingsPage.tsx
|
||||
3. settings_views.py
|
||||
4. urls.py
|
||||
5. Dashboard.tsx
|
||||
6. App.tsx
|
||||
|
||||
### Files Created
|
||||
1. SiteSetupChecklist.tsx
|
||||
|
||||
### Files Deleted
|
||||
1. Manage.tsx
|
||||
2. `frontend/src/pages/Sites/Builder/` (empty folder)
|
||||
|
||||
|
||||
@@ -310,4 +310,42 @@ Not part of current phase. No issues to address.
|
||||
| Error recovery | API to retry failed items |
|
||||
| Notification logging | Ensure AI runs are logged for notification display |
|
||||
|
||||
---
|
||||
---
|
||||
## After Implementation
|
||||
Section 3 implementation is complete. Here's a summary of all changes made:
|
||||
|
||||
## Section 3 WORKFLOW Modules - Implementation Complete ✅
|
||||
|
||||
### Completed Tasks:
|
||||
|
||||
1. **Delete KeywordOpportunities.tsx** ✅
|
||||
- Deleted the orphaned page file
|
||||
- Removed import and route from App.tsx
|
||||
- Updated PAGES.md documentation
|
||||
|
||||
2. **Add cluster ideas badge** ✅
|
||||
- Modified clusters.config.tsx
|
||||
- Ideas column now shows badge: "X ideas" (green) or "No ideas" (gray)
|
||||
|
||||
3. **Fix ContentView duplicate tags/categories** ✅
|
||||
- Modified ContentViewTemplate.tsx
|
||||
- Removed duplicate tags/categories section that appeared below metadata
|
||||
|
||||
4. **Fix progress modal placeholder texts** ✅
|
||||
- Modified ProgressModal.tsx
|
||||
- Fixed "X" placeholders in step labels with cleaner fallbacks
|
||||
- "Mapping Content for X Image Prompts" → "Mapping content for image prompts"
|
||||
- "Writing X In‑article Image Prompts" → "Writing In‑article Image Prompts"
|
||||
- Success message fallback cleaned up
|
||||
|
||||
5. **Add queued count to Ideas** ✅
|
||||
- Already implemented via headerMetrics showing New/Queued/Completed counts
|
||||
|
||||
6. **Clickable cluster in Ideas** ✅
|
||||
- Modified ideas.config.tsx
|
||||
- Cluster name now links to `/planner/clusters/:id`
|
||||
|
||||
### Documentation Updated:
|
||||
- CHANGELOG.md - Added v1.1.5 section
|
||||
- ENDPOINTS.md - Added Content Settings API docs
|
||||
- PAGES.md - Removed KeywordOpportunities, updated version
|
||||
Reference in New Issue
Block a user