/** * Industries, Sectors & Keywords Setup Page * Merged page combining: * - Industry selection * - Sector selection (from selected industry) * - Keyword opportunities (filtered by selected industry/sectors) * * Saves selections to user account for use in site creation */ import { useState, useEffect, useCallback, useMemo } from 'react'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchIndustries, Industry, fetchSeedKeywords, SeedKeyword, fetchAccountSetting, createAccountSetting, updateAccountSetting, AccountSettingsError } from '../../services/api'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; import Button from '../../components/ui/button/Button'; import { PieChartIcon, CheckCircleIcon, BoltIcon } from '../../icons'; import { Tooltip } from '../../components/ui/tooltip/Tooltip'; import { Tabs, TabList, Tab, TabPanel } from '../../components/ui/tabs/Tabs'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { usePageSizeStore } from '../../store/pageSizeStore'; import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty'; interface IndustryWithData extends Industry { keywordsCount: number; totalVolume: number; sectors?: Array<{ slug: string; name: string; description?: string }>; } interface UserPreferences { selectedIndustry?: string; selectedSectors?: string[]; selectedKeywords?: number[]; } // Format volume with k for thousands and m for millions const formatVolume = (volume: number): string => { if (volume >= 1000000) { return `${(volume / 1000000).toFixed(1)}m`; } else if (volume >= 1000) { return `${(volume / 1000).toFixed(1)}k`; } return volume.toString(); }; const getAccountSettingsPreferenceMessage = (error: AccountSettingsError): string => { switch (error.type) { case 'ACCOUNT_SETTINGS_VALIDATION_ERROR': return error.message || 'The saved preferences could not be loaded because the data is invalid.'; default: return error.message || 'Unable to load your saved preferences right now.'; } }; export default function IndustriesSectorsKeywords() { const toast = useToast(); const { pageSize } = usePageSizeStore(); // Data state const [industries, setIndustries] = useState([]); const [selectedIndustry, setSelectedIndustry] = useState(null); const [selectedSectors, setSelectedSectors] = useState([]); const [seedKeywords, setSeedKeywords] = useState([]); const [loading, setLoading] = useState(true); const [keywordsLoading, setKeywordsLoading] = useState(false); const [saving, setSaving] = useState(false); // Keywords table state const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); const [searchTerm, setSearchTerm] = useState(''); const [intentFilter, setIntentFilter] = useState(''); const [difficultyFilter, setDifficultyFilter] = useState(''); const [selectedKeywordIds, setSelectedKeywordIds] = useState([]); // Load industries on mount useEffect(() => { loadIndustries(); loadUserPreferences(); }, []); // Load user preferences from account settings const loadUserPreferences = async () => { try { const setting = await fetchAccountSetting('user_preferences'); const preferences = setting.config as UserPreferences | undefined; if (preferences) { if (preferences.selectedIndustry) { // Find and select the industry const industry = industries.find(i => i.slug === preferences.selectedIndustry); if (industry) { setSelectedIndustry(industry); if (preferences.selectedSectors) { setSelectedSectors(preferences.selectedSectors); } } } } } catch (error: any) { if (error instanceof AccountSettingsError) { if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') { // Preferences don't exist yet - this is expected for new users return; } // For other errors (500, etc.), silently handle - user can still use the page // Don't show error toast for server errors - graceful degradation return; } // For non-AccountSettingsError errors, silently handle - graceful degradation } }; // Load industries const loadIndustries = async () => { try { setLoading(true); const response = await fetchIndustries(); const industriesList = response.industries || []; // Fetch keywords to calculate counts let allKeywords: SeedKeyword[] = []; try { const keywordsResponse = await fetchSeedKeywords({ page_size: 1000, }); allKeywords = keywordsResponse.results || []; } catch (error) { console.warn('Failed to fetch keywords for counts:', error); } // Process each industry with its keywords data const industriesWithData = industriesList.map((industry) => { const industryKeywords = allKeywords.filter( (kw: SeedKeyword) => kw.industry_name === industry.name ); const totalVolume = industryKeywords.reduce( (sum, kw) => sum + (kw.volume || 0), 0 ); return { ...industry, keywordsCount: industryKeywords.length, totalVolume, }; }); setIndustries(industriesWithData.filter(i => i.keywordsCount > 0)); } catch (error: any) { toast.error(`Failed to load industries: ${error.message}`); } finally { setLoading(false); } }; // Load sectors for selected industry const loadSectorsForIndustry = useCallback(async () => { if (!selectedIndustry) { return; } try { // Fetch sectors from the industry's related data // The industry object should have sectors in the response const response = await fetchIndustries(); const industryData = response.industries?.find(i => i.slug === selectedIndustry.slug); if (industryData?.sectors) { // Sectors are already in the industry data } } catch (error: any) { console.error('Error loading sectors:', error); } }, [selectedIndustry]); // Load keywords when industry/sectors change useEffect(() => { if (selectedIndustry) { loadKeywords(); } else { setSeedKeywords([]); setTotalCount(0); } }, [selectedIndustry, selectedSectors, currentPage, searchTerm, intentFilter, difficultyFilter]); // Load seed keywords const loadKeywords = async () => { if (!selectedIndustry) { return; } setKeywordsLoading(true); try { const filters: any = { industry_name: selectedIndustry.name, page: currentPage, page_size: pageSize, }; if (selectedSectors.length > 0) { // Filter by selected sectors if any // Note: API might need sector_name filter } if (searchTerm) { filters.search = searchTerm; } if (intentFilter) { filters.intent = intentFilter; } 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; } } } const data = await fetchSeedKeywords(filters); setSeedKeywords(data.results || []); setTotalCount(data.count || 0); setTotalPages(Math.ceil((data.count || 0) / pageSize)); } catch (error: any) { toast.error(`Failed to load keywords: ${error.message}`); } finally { setKeywordsLoading(false); } }; // Handle industry selection const handleIndustrySelect = (industry: Industry) => { setSelectedIndustry(industry); setSelectedSectors([]); // Reset sectors when industry changes setCurrentPage(1); }; // Handle sector toggle const handleSectorToggle = (sectorSlug: string) => { setSelectedSectors(prev => prev.includes(sectorSlug) ? prev.filter(s => s !== sectorSlug) : [...prev, sectorSlug] ); setCurrentPage(1); }; // Save preferences to account const handleSavePreferences = async () => { if (!selectedIndustry) { toast.error('Please select an industry first'); return; } setSaving(true); try { const preferences: UserPreferences = { selectedIndustry: selectedIndustry.slug, selectedSectors: selectedSectors, selectedKeywords: selectedKeywordIds.map(id => parseInt(id)), }; // Try to update existing setting, or create if it doesn't exist try { await updateAccountSetting('user_preferences', { config: preferences, is_active: true, }); } catch (error: any) { if (error instanceof AccountSettingsError && error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') { await createAccountSetting({ key: 'user_preferences', config: preferences, is_active: true, }); } else { throw error; } } toast.success('Preferences saved successfully! These will be used when creating new sites.'); } catch (error: any) { if (error instanceof AccountSettingsError) { toast.error(getAccountSettingsPreferenceMessage(error)); } else { toast.error(`Failed to save preferences: ${error.message}`); } } finally { setSaving(false); } }; // Get available sectors for selected industry const availableSectors = useMemo(() => { if (!selectedIndustry) return []; // Get sectors from industry data or fetch separately // For now, we'll use the industry's sectors if available return selectedIndustry.sectors || []; }, [selectedIndustry]); // Keywords table columns const keywordColumns = useMemo(() => [ { key: 'keyword', label: 'Keyword', sortable: true, render: (row: SeedKeyword) => (
{row.keyword}
), }, { key: 'industry_name', label: 'Industry', sortable: true, render: (row: SeedKeyword) => ( {row.industry_name} ), }, { key: 'sector_name', label: 'Sector', sortable: true, render: (row: SeedKeyword) => ( {row.sector_name} ), }, { key: 'volume', label: 'Volume', sortable: true, render: (row: SeedKeyword) => ( {row.volume ? formatVolume(row.volume) : '-'} ), }, { key: 'difficulty', label: 'Difficulty', sortable: true, render: (row: SeedKeyword) => { const difficulty = row.difficulty || 0; const label = difficulty < 30 ? 'Easy' : difficulty < 70 ? 'Medium' : 'Hard'; const color = difficulty < 30 ? 'success' : difficulty < 70 ? 'warning' : 'error'; return {label}; }, }, { key: 'intent', label: 'Intent', sortable: true, render: (row: SeedKeyword) => ( {row.intent || 'N/A'} ), }, ], []); return ( <> , color: 'blue' }} hideSiteSector={true} />
{(activeTab, setActiveTab) => ( <> setActiveTab('industries')}> Industries setActiveTab('sectors')} disabled={!selectedIndustry}> Sectors {selectedIndustry && `(${selectedSectors.length})`} setActiveTab('keywords')} disabled={!selectedIndustry}> Keywords {selectedIndustry && `(${totalCount})`}

Select Your Industry

Choose the industry that best matches your business. This will be used as a default when creating new sites.

{loading ? (
Loading industries...
) : (
{industries.map((industry) => ( handleIndustrySelect(industry)} >

{industry.name}

{selectedIndustry?.slug === industry.slug && ( )}
{industry.description && (

{industry.description}

)}
{industry.sectors?.length || 0} sectors
{industry.keywordsCount || 0} keywords
{industry.totalVolume > 0 && (
Total volume: {formatVolume(industry.totalVolume)}
)}
))}
)}

Select Sectors for {selectedIndustry?.name}

Choose one or more sectors within your selected industry. These will be used as defaults when creating new sites.

{!selectedIndustry ? (
Please select an industry first
) : availableSectors.length === 0 ? (
No sectors available for this industry
) : (
{availableSectors.map((sector) => ( handleSectorToggle(sector.slug)} >

{sector.name}

{selectedSectors.includes(sector.slug) && ( )}
{sector.description && (

{sector.description}

)}
))}
)}

Keyword Opportunities for {selectedIndustry?.name}

Browse and select keywords that match your selected industry and sectors. These will be available when creating new sites.

{!selectedIndustry ? (
Please select an industry first
) : ( { if (key === 'search') setSearchTerm(value as string); else if (key === 'intent') setIntentFilter(value as string); else if (key === 'difficulty') setDifficultyFilter(value as string); setCurrentPage(1); }} pagination={{ currentPage, totalPages, totalCount, onPageChange: setCurrentPage, }} selection={{ selectedIds: selectedKeywordIds, onSelectionChange: setSelectedKeywordIds, }} /> )}
)}
{/* Save Button */} {selectedIndustry && (
)}
); }