/** * Keyword Library Page (formerly Add Keywords) * Browse global seed keywords filtered by site's industry and sectors * Add keywords to workflow (creates Keywords records in planner) * Features: High Opportunity Keywords section, Browse all keywords, CSV import * Coming soon: Ahrefs keyword research integration (March/April 2026) */ import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { usePageLoading } from '../../context/PageLoadingContext'; import WorkflowGuide from '../../components/onboarding/WorkflowGuide'; import { fetchSeedKeywords, SeedKeyword, SeedKeywordResponse, fetchSites, Site, addSeedKeywordsToWorkflow, fetchSiteSectors, fetchIndustries, fetchKeywords, } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; import { BoltIcon, PlusIcon, CheckCircleIcon, ShootingStarIcon, DocsIcon } from '../../icons'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { usePageSizeStore } from '../../store/pageSizeStore'; import { getDifficultyNumber, getDifficultyRange, getDifficultyLabelFromNumber } from '../../utils/difficulty'; import { useSiteStore } from '../../store/siteStore'; import { useSectorStore } from '../../store/sectorStore'; import { useAuthStore } from '../../store/authStore'; import Button from '../../components/ui/button/Button'; import { Modal } from '../../components/ui/modal'; import FileInput from '../../components/form/input/FileInput'; import Label from '../../components/form/Label'; import { Card } from '../../components/ui/card'; import { Spinner } from '../../components/ui/spinner/Spinner'; // High Opportunity Keywords types interface SectorKeywordOption { type: 'high-volume' | 'low-difficulty'; label: string; keywords: SeedKeyword[]; added: boolean; keywordCount: number; } interface SectorKeywordData { sectorSlug: string; sectorName: string; sectorId: number; options: SectorKeywordOption[]; } export default function IndustriesSectorsKeywords() { const toast = useToast(); const { startLoading, stopLoading } = usePageLoading(); const { activeSite } = useSiteStore(); const { activeSector, loadSectorsForSite } = useSectorStore(); const { pageSize } = usePageSizeStore(); const { user } = useAuthStore(); // Data state const [sites, setSites] = useState([]); const [seedKeywords, setSeedKeywords] = useState<(SeedKeyword & { isAdded?: boolean })[]>([]); const [showContent, setShowContent] = useState(false); const [selectedIds, setSelectedIds] = useState([]); // Track recently added keywords to preserve their state during reload const recentlyAddedRef = useRef>(new Set()); // High Opportunity Keywords state const [showHighOpportunity, setShowHighOpportunity] = useState(true); const [loadingOpportunityKeywords, setLoadingOpportunityKeywords] = useState(false); const [highOpportunityLoaded, setHighOpportunityLoaded] = useState(false); const [sectorKeywordData, setSectorKeywordData] = useState([]); const [addingOption, setAddingOption] = useState(null); // Browse table state const [showBrowseTable, setShowBrowseTable] = useState(false); // Ahrefs banner state const [showAhrefsBanner, setShowAhrefsBanner] = useState(true); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); // Sorting state const [sortBy, setSortBy] = useState('keyword'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); // Filter state const [searchTerm, setSearchTerm] = useState(''); const [countryFilter, setCountryFilter] = useState(''); const [difficultyFilter, setDifficultyFilter] = useState(''); const [showNotAddedOnly, setShowNotAddedOnly] = useState(false); // Keyword count tracking const [addedCount, setAddedCount] = useState(0); const [availableCount, setAvailableCount] = useState(0); // Navigation const navigate = useNavigate(); // Import modal state const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isImporting, setIsImporting] = useState(false); const [importFile, setImportFile] = useState(null); // Load sites on mount useEffect(() => { loadInitialData(); }, []); const loadInitialData = async () => { try { startLoading('Loading...'); const response = await fetchSites(); const activeSites = (response.results || []).filter(site => site.is_active); setSites(activeSites); // Don't stop loading here - let High Opportunity Keywords loading finish } catch (error: any) { toast.error(`Failed to load sites: ${error.message}`); stopLoading(); } }; // Handle site added from WorkflowGuide const handleSiteAdded = () => { loadInitialData(); }; // Load High Opportunity Keywords for active site const loadHighOpportunityKeywords = useCallback(async () => { if (!activeSite || !activeSite.industry) { setSectorKeywordData([]); return; } setLoadingOpportunityKeywords(true); try { // 1. Get site sectors const siteSectors = await fetchSiteSectors(activeSite.id); // 2. Get industry data const industriesResponse = await fetchIndustries(); const industry = industriesResponse.industries?.find( i => i.id === activeSite.industry || i.slug === activeSite.industry_slug ); if (!industry?.id) { console.warn('Could not find industry information'); setSectorKeywordData([]); return; } // 3. Get already-attached keywords to mark as added const attachedSeedKeywordIds = new Set(); for (const sector of siteSectors) { try { const keywordsData = await fetchKeywords({ site_id: activeSite.id, sector_id: sector.id, page_size: 1000, }); (keywordsData.results || []).forEach((k: any) => { 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); } } // 4. Build sector keyword data const sectorData: SectorKeywordData[] = []; for (const siteSector of siteSectors) { if (!siteSector.is_active) continue; // Fetch all keywords for this sector const response = await fetchSeedKeywords({ industry: industry.id, sector: siteSector.industry_sector, page_size: 500, }); const sectorKeywords = response.results; // Top 50 by highest volume const highVolumeKeywords = [...sectorKeywords] .sort((a, b) => (b.volume || 0) - (a.volume || 0)) .slice(0, 50); // Top 50 by lowest difficulty - exclude keywords already in high volume to avoid duplicates const highVolumeIds = new Set(highVolumeKeywords.map(kw => kw.id)); const lowDifficultyKeywords = [...sectorKeywords] .filter(kw => !highVolumeIds.has(kw.id)) // Exclude duplicates from high volume .sort((a, b) => (a.difficulty || 100) - (b.difficulty || 100)) .slice(0, 50); // Check if all keywords in each option are already added const hvAdded = highVolumeKeywords.length > 0 && highVolumeKeywords.every(kw => attachedSeedKeywordIds.has(Number(kw.id)) ); const ldAdded = lowDifficultyKeywords.length > 0 && lowDifficultyKeywords.every(kw => attachedSeedKeywordIds.has(Number(kw.id)) ); sectorData.push({ sectorSlug: siteSector.slug, sectorName: siteSector.name, sectorId: siteSector.id, options: [ { type: 'high-volume', label: 'Top 50 High Volume', keywords: highVolumeKeywords, added: hvAdded, keywordCount: highVolumeKeywords.length, }, { type: 'low-difficulty', label: 'Top 50 Low Difficulty', keywords: lowDifficultyKeywords, added: ldAdded, keywordCount: lowDifficultyKeywords.length, }, ], }); } setSectorKeywordData(sectorData); setHighOpportunityLoaded(true); stopLoading(); // Stop global loading spinner after High Opportunity is ready } catch (error: any) { console.error('Failed to load high opportunity keywords:', error); toast.error(`Failed to load high opportunity keywords: ${error.message}`); setHighOpportunityLoaded(true); // Still mark as loaded even on error stopLoading(); // Stop spinner even on error } finally { setLoadingOpportunityKeywords(false); } }, [activeSite, toast, stopLoading]); // Load sectors for active site useEffect(() => { if (activeSite?.id) { loadSectorsForSite(activeSite.id); } }, [activeSite?.id, loadSectorsForSite]); // Load High Opportunity Keywords when site changes useEffect(() => { if (activeSite && activeSite.id && showHighOpportunity) { loadHighOpportunityKeywords(); } }, [activeSite?.id, showHighOpportunity, loadHighOpportunityKeywords]); // Load keyword counts for display (lightweight - just get count from API) const loadKeywordCounts = useCallback(async () => { if (!activeSite || !activeSite.industry) { setAddedCount(0); setAvailableCount(0); return; } try { // Get attached keywords count const sectors = await fetchSiteSectors(activeSite.id); let totalAdded = 0; for (const sector of sectors) { try { const keywordsData = await fetchKeywords({ site_id: activeSite.id, sector_id: sector.id, page_size: 1, }); totalAdded += keywordsData.count || 0; } catch (err) { console.warn(`Could not fetch keyword count for sector ${sector.id}:`, err); } } // Get total available keywords from API const filters: any = { industry: activeSite.industry, page_size: 1, page: 1, }; if (activeSector && activeSector.industry_sector) { filters.sector = activeSector.industry_sector; } const data = await fetchSeedKeywords(filters); const totalAvailable = data.count || 0; setAddedCount(totalAdded); setAvailableCount(totalAvailable); } catch (err) { console.warn('Could not fetch keyword counts:', err); } }, [activeSite, activeSector]); // Load counts on mount and when site/sector changes useEffect(() => { if (activeSite) { loadKeywordCounts(); } }, [activeSite, activeSector, loadKeywordCounts]); // Load seed keywords with optimized API pagination - only load current page const loadSeedKeywords = useCallback(async () => { if (!activeSite || !activeSite.industry) { setSeedKeywords([]); setTotalCount(0); setTotalPages(1); setShowContent(true); return; } setShowContent(false); try { // Get already-attached keywords for marking (lightweight check) const attachedSeedKeywordIds = new Set(); try { const sectors = await fetchSiteSectors(activeSite.id); // Check keywords in all sectors (needed for isAdded flag) 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) => { 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); } } } catch (err) { console.warn('Could not fetch sectors or attached keywords:', err); } // Build API filters - use server-side pagination const pageSizeNum = pageSize || 25; const filters: any = { industry: activeSite.industry, page_size: pageSizeNum, page: currentPage, }; // Add sector filter if active sector is selected if (activeSector && activeSector.industry_sector) { filters.sector = activeSector.industry_sector; } if (searchTerm) filters.search = searchTerm; if (countryFilter) filters.country = countryFilter; // Apply difficulty filter (if API supports it, otherwise we'll filter client-side) if (difficultyFilter) { const difficultyNum = parseInt(difficultyFilter); const label = getDifficultyLabelFromNumber(difficultyNum); if (label !== null) { const range = getDifficultyRange(label); if (range) { filters.difficulty_min = range.min; filters.difficulty_max = range.max; } } } // Add sorting to API request if (sortBy && sortDirection) { const sortPrefix = sortDirection === 'desc' ? '-' : ''; filters.ordering = `${sortPrefix}${sortBy}`; } // Fetch only current page from API const data: SeedKeywordResponse = await fetchSeedKeywords(filters); // Mark already-attached keywords const results = (data.results || []).map(sk => { const isAdded = attachedSeedKeywordIds.has(Number(sk.id)) || recentlyAddedRef.current.has(Number(sk.id)); return { ...sk, isAdded: Boolean(isAdded) }; }); // Calculate counts from API response const apiTotalCount = data.count || 0; // For added count, we need to check all attached keywords const totalAdded = attachedSeedKeywordIds.size; const actualAvailable = apiTotalCount; setAddedCount(totalAdded); setAvailableCount(actualAvailable); // Apply "not yet added" filter client-side (if API doesn't support it) let filteredResults = results; if (showNotAddedOnly) { filteredResults = results.filter(sk => !sk.isAdded); } setSeedKeywords(filteredResults); setTotalCount(apiTotalCount); setTotalPages(Math.ceil(apiTotalCount / pageSizeNum)); setShowContent(true); } catch (error: any) { console.error('Error loading seed keywords:', error); toast.error(`Failed to load keywords: ${error.message}`); setSeedKeywords([]); setTotalCount(0); setTotalPages(1); setAddedCount(0); setAvailableCount(0); setShowContent(true); } }, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, showNotAddedOnly, sortBy, sortDirection, toast]); // Load data only when browse table is shown and filters change useEffect(() => { if (activeSite && showBrowseTable) { loadSeedKeywords(); } }, [loadSeedKeywords, activeSite, showBrowseTable]); // Debounced search useEffect(() => { const timer = setTimeout(() => { setCurrentPage(1); }, 500); return () => clearTimeout(timer); }, [searchTerm]); // Reset to page 1 on pageSize change useEffect(() => { setCurrentPage(1); }, [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 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); }); } // Only track and mark as added if actually created if (result.created > 0) { // Track as recently added seedKeywordIds.forEach(id => { recentlyAddedRef.current.add(id); }); // Update state - mark as added setSeedKeywords(prevKeywords => prevKeywords.map(kw => seedKeywordIds.includes(kw.id) ? { ...kw, isAdded: true } : kw ) ); } // Clear selection setSelectedIds([]); } else { // Show user-friendly error message (errors array already contains clean messages) const errorMsg = result.errors?.[0] || 'Unable to add keywords. Please try again.'; toast.error(errorMsg); } } catch (error: any) { // Show user-friendly error message (error.message is already cleaned) toast.error(error.message || 'Unable to add keywords. Please try again.'); } }, [activeSite, activeSector, toast]); // Handle bulk add selected 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 const handleAddAll = useCallback(async () => { if (!activeSite || !activeSite.industry) { toast.error('Please select an active site first'); return; } const availableKeywords = seedKeywords.filter(sk => !sk.isAdded); if (availableKeywords.length === 0) { toast.error('All keywords are already added to workflow'); return; } const seedKeywordIds = availableKeywords.map(sk => sk.id); await handleAddToWorkflow(seedKeywordIds); }, [activeSite, seedKeywords, handleAddToWorkflow, toast]); // Handle import click const handleImportClick = useCallback(() => { setIsImportModalOpen(true); }, []); // Handle adding High Opportunity Keywords for a sector const handleAddSectorKeywords = useCallback(async ( sectorSlug: string, optionType: 'high-volume' | 'low-difficulty' ) => { const sector = sectorKeywordData.find(s => s.sectorSlug === sectorSlug); if (!sector || !activeSite) return; const option = sector.options.find(o => o.type === optionType); if (!option || option.added || option.keywords.length === 0) return; const addingKey = `${sectorSlug}-${optionType}`; setAddingOption(addingKey); try { // Get currently attached keywords to filter out duplicates const attachedSeedKeywordIds = new Set(); try { const sectors = await fetchSiteSectors(activeSite.id); for (const s of sectors) { try { const keywordsData = await fetchKeywords({ site_id: activeSite.id, sector_id: s.id, page_size: 1000, }); (keywordsData.results || []).forEach((k: any) => { 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 ${s.id}:`, err); } } } catch (err) { console.warn('Could not fetch attached keywords:', err); } // Filter out already-added keywords const seedKeywordIds = option.keywords .filter(kw => !attachedSeedKeywordIds.has(Number(kw.id))) .map(kw => kw.id); if (seedKeywordIds.length === 0) { toast.warning('All keywords from this set are already in your workflow.'); // Mark as added since all are already there setSectorKeywordData(prev => prev.map(s => s.sectorSlug === sectorSlug ? { ...s, options: s.options.map(o => o.type === optionType ? { ...o, added: true } : o ), } : s ) ); setAddingOption(null); return; } const result = await addSeedKeywordsToWorkflow( seedKeywordIds, activeSite.id, sector.sectorId ); if (result.success && result.created > 0) { // Mark option as added setSectorKeywordData(prev => prev.map(s => s.sectorSlug === sectorSlug ? { ...s, options: s.options.map(o => o.type === optionType ? { ...o, added: true } : o ), } : s ) ); let message = `Added ${result.created} keywords to ${sector.sectorName}`; if (result.skipped && result.skipped > 0) { message += ` (${result.skipped} already exist)`; } toast.success(message); // Reload the main table to reflect changes loadSeedKeywords(); } else if (result.errors && result.errors.length > 0) { toast.error(result.errors[0]); } else { toast.warning('No keywords were added. They may already exist in your workflow.'); } } catch (err: any) { toast.error(err.message || 'Failed to add keywords to workflow'); } finally { setAddingOption(null); } }, [sectorKeywordData, activeSite, toast, loadSeedKeywords]); // Handle import file upload const handleImportSubmit = useCallback(async () => { if (!importFile) { toast.error('Please select a file to import'); return; } setIsImporting(true); try { const formData = new FormData(); formData.append('file', importFile); const response = await fetch('/api/v1/auth/seed-keywords/import_seed_keywords/', { method: 'POST', headers: { 'Authorization': `Token ${localStorage.getItem('authToken')}`, }, body: formData, }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Import failed'); } const result = await response.json(); toast.success(`Successfully imported ${result.created || 0} keywords`); // Reset and close modal setImportFile(null); setIsImportModalOpen(false); // Reload keywords if (activeSite) { loadSeedKeywords(); } } catch (error: any) { console.error('Import error:', error); toast.error(`Import failed: ${error.message}`); } finally { setIsImporting(false); } }, [importFile, toast, activeSite, loadSeedKeywords]); // Page config const pageConfig = useMemo(() => { const showSectorColumn = !activeSector; return { columns: [ { key: 'keyword', label: 'Keyword', sortable: true, sortField: 'keyword', }, ...(showSectorColumn ? [{ key: 'sector_name', label: 'Sector', sortable: false, render: (_value: string, row: SeedKeyword) => ( {row.sector_name || '-'} ), }] : []), { 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' ? ( {difficultyNum} ) : ( difficultyNum ); }, }, { key: 'country', label: 'Country', sortable: true, sortField: 'country', render: (value: string) => { return ( {value || '-'} ); }, }, { key: 'actions', label: '', sortable: false, align: 'right' as const, render: (_value: any, row: SeedKeyword & { isAdded?: boolean }) => { const isDisabled = !activeSector || row.isAdded; const buttonText = row.isAdded ? 'Added' : 'Add to Workflow'; return (
); }, }, ], filters: [ { key: 'search', label: 'Search', type: 'text' as const, placeholder: 'Search keywords...', }, { key: 'country', label: 'Country', type: 'select' as const, 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' as const, 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: 'showNotAddedOnly', label: 'Status', type: 'select' as const, options: [ { value: '', label: 'All Keywords' }, { value: 'true', label: 'Not Yet Added Only' }, ], }, ], bulkActions: !activeSector ? [] : [ { key: 'add_selected_to_workflow', label: 'Add Selected to Workflow', variant: 'primary' as const, }, ], }; }, [activeSector, handleAddToWorkflow]); // High Opportunity Keywords Component const HighOpportunityKeywordsSection = () => { if (!activeSite || sectorKeywordData.length === 0) return null; const addedCount = sectorKeywordData.reduce( (acc, s) => acc + s.options .filter(o => o.added) .reduce((sum, o) => sum + o.keywordCount, 0), 0 ); const totalKeywordCount = sectorKeywordData.reduce( (acc, s) => acc + s.options.reduce((sum, o) => sum + o.keywordCount, 0), 0 ); const allAdded = totalKeywordCount > 0 && addedCount === totalKeywordCount; // Auto-collapse when all keywords are added useEffect(() => { if (allAdded && showHighOpportunity) { // Keep it open for a moment to show success, then collapse const timer = setTimeout(() => { setShowHighOpportunity(false); }, 2000); return () => clearTimeout(timer); } }, [allAdded]); // Show collapsed state with option to expand if (!showHighOpportunity) { return (

Quick-Start Keywords Added Successfully

{addedCount} keywords added to your workflow

); } return (
{/* Header */}

Quick-Start Keywords — Complimentary

Pre-Vetted
{!allAdded && ( )}

Ready-to-use keywords to jumpstart your content — no research needed. Simply add to your workflow and start creating.

{/* Loading State */} {loadingOpportunityKeywords ? (
) : ( <> {/* Sector Grid */}
{sectorKeywordData.map((sector) => (
{/* Sector Name */}

{sector.sectorName}

{/* Options Cards */} {sector.options.map((option) => { const addingKey = `${sector.sectorSlug}-${option.type}`; const isAdding = addingOption === addingKey; return ( {/* Option Header */}
{option.label}

{option.keywordCount} keywords

{option.added ? ( Added ) : ( )}
{/* Sample Keywords */}
{option.keywords.slice(0, 3).map((kw) => ( {kw.keyword} ))} {option.keywordCount > 3 && ( +{option.keywordCount - 3} more )}
); })}
))}
{/* Success Summary */} {addedCount > 0 && (
{addedCount} keywords added to your workflow
)} )}
); }; // Show WorkflowGuide if no sites and High Opportunity has loaded (to avoid flashing) if (highOpportunityLoaded && sites.length === 0) { return ( <> , color: 'blue' }} />
); } return ( <> , color: 'blue' }} /> {/* High Opportunity Keywords Section - Loads First */} {/* Browse Individual Keywords Section - Shows after High Opportunity is loaded */} {highOpportunityLoaded && !showBrowseTable && activeSite && (

Browse Individual Keywords

💡 Recommended: Start with the complimentary Quick-Start Keywords above to accelerate your workflow. They're pre-vetted and ready to use immediately. Once added, you can browse our full library below for additional targeted keywords.

)} {/* Show info banner when no sector is selected and table is shown */} {showBrowseTable && !activeSector && activeSite && (

Choose a Topic Area First

Pick a topic area first, then add keywords - You need to choose what you're writing about before adding search terms to target

)} {/* Keywords Browse Table - Only show when user clicks browse button */} {showBrowseTable && ( {addedCount} in workflow
{availableCount} available {addedCount > 0 && ( <>
)}
) : undefined} 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); } else if (key === 'showNotAddedOnly') { setShowNotAddedOnly(stringValue === 'true'); setCurrentPage(1); } }} onBulkAction={async (actionKey: string, ids: string[]) => { if (actionKey === 'add_selected_to_workflow') { await handleBulkAddSelected(ids); } }} bulkActions={pageConfig.bulkActions} pagination={{ currentPage, totalPages, totalCount, onPageChange: setCurrentPage, }} sorting={{ sortBy, sortDirection, onSort: handleSort, }} selection={{ selectedIds, onSelectionChange: setSelectedIds, }} onEdit={undefined} onDelete={undefined} /> )} {/* Ahrefs Coming Soon Banner - Shows at bottom after High Opportunity is loaded */} {highOpportunityLoaded && showAhrefsBanner && (

Ahrefs Keyword Research - Coming March/April 2026

Soon you'll be able to research keywords directly with Ahrefs API, pulling fresh data with volume, difficulty, and competition metrics. For now, browse our curated keyword library above.

)} {/* Import Modal */} { if (!isImporting) { setIsImportModalOpen(false); setImportFile(null); } }} >

Import Seed Keywords

Expected columns: keyword, volume, difficulty, country, industry_name, sector_name

{ const file = e.target.files?.[0]; if (file) { setImportFile(file); } }} disabled={isImporting} /> {importFile && (

Selected: {importFile.name}

)}
); }