diff --git a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx index 2ca92e5e..2e20bcdb 100644 --- a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx +++ b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx @@ -21,45 +21,24 @@ import { Site, addSeedKeywordsToWorkflow, fetchSiteSectors, - fetchIndustries, fetchKeywords, fetchSectorStats, SectorStats, - bulkAddKeywordsToSite, } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; -import { BoltIcon, PlusIcon, CheckCircleIcon, ShootingStarIcon, DocsIcon } from '../../icons'; +import { BoltIcon, ShootingStarIcon } 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'; import { SectorMetricGrid, StatType } from '../../components/keywords-library/SectorMetricCard'; import SmartSuggestions, { buildSmartSuggestions } from '../../components/keywords-library/SmartSuggestions'; -import BulkAddConfirmation from '../../components/keywords-library/BulkAddConfirmation'; - -// 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(); @@ -67,7 +46,6 @@ export default function IndustriesSectorsKeywords() { const { activeSite } = useSiteStore(); const { activeSector, loadSectorsForSite } = useSectorStore(); const { pageSize } = usePageSizeStore(); - const { user } = useAuthStore(); // Data state const [sites, setSites] = useState([]); @@ -87,16 +65,6 @@ export default function IndustriesSectorsKeywords() { const [bulkAddKeywordIds, setBulkAddKeywordIds] = useState([]); const [bulkAddStatLabel, setBulkAddStatLabel] = useState(); - // 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); @@ -150,122 +118,6 @@ export default function IndustriesSectorsKeywords() { 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 fetchKeywordsLibrary({ - 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) { @@ -273,13 +125,6 @@ export default function IndustriesSectorsKeywords() { } }, [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) { @@ -518,12 +363,12 @@ export default function IndustriesSectorsKeywords() { } }, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, showNotAddedOnly, sortBy, sortDirection, toast]); - // Load data only when browse table is shown and filters change + // Load data when site/sector/filters change (show table by default per plan) useEffect(() => { - if (activeSite && showBrowseTable) { + if (activeSite) { loadSeedKeywords(); } - }, [loadSeedKeywords, activeSite, showBrowseTable]); + }, [loadSeedKeywords, activeSite]); // Debounced search useEffect(() => { @@ -680,111 +525,6 @@ export default function IndustriesSectorsKeywords() { 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) { @@ -992,207 +732,14 @@ export default function IndustriesSectorsKeywords() { }; }, [activeSector, handleAddToWorkflow]); - // High Opportunity Keywords Component - const HighOpportunityKeywordsSection = () => { - if (!activeSite || sectorKeywordData.length === 0) return null; + // Build smart suggestions from sector stats + const smartSuggestions = useMemo(() => { + if (!sectorStats) return []; + return buildSmartSuggestions(sectorStats, { showOnlyWithResults: true }); + }, [sectorStats]); - 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) { + // Show WorkflowGuide if no sites + if (sites.length === 0) { return ( <> @@ -1218,7 +765,6 @@ export default function IndustriesSectorsKeywords() { } setActiveStatFilter(statType); - setShowBrowseTable(true); setCurrentPage(1); // Apply filters based on stat type @@ -1261,21 +807,10 @@ export default function IndustriesSectorsKeywords() { } }; - // Build smart suggestions from sector stats - const smartSuggestions = useMemo(() => { - if (!sectorStats) return []; - return buildSmartSuggestions(sectorStats, { showOnlyWithResults: true }); - }, [sectorStats]); - return ( <> - {/* TEST TEXT - REMOVE AFTER VERIFICATION */} -
- Test Text -
- , color: 'blue' }} @@ -1317,41 +852,9 @@ export default function IndustriesSectorsKeywords() { /> )} - - {/* 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 && ( + {/* Show info banner when no sector is selected */} + {!activeSector && activeSite && (
@@ -1373,8 +876,8 @@ export default function IndustriesSectorsKeywords() {
)} - {/* Keywords Browse Table - Only show when user clicks browse button */} - {showBrowseTable && ( + {/* Keywords Table - Shown by default (per plan) */} + {activeSite && ( )} - {/* Ahrefs Coming Soon Banner - Shows at bottom after High Opportunity is loaded */} - {highOpportunityLoaded && showAhrefsBanner && ( + {/* Ahrefs Coming Soon Banner */} + {showAhrefsBanner && (