keywrod slibrary page dsigning

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-18 19:56:49 +00:00
parent aa03e15eea
commit 4cf27fa875
9 changed files with 1151 additions and 323 deletions

View File

@@ -45,7 +45,7 @@ export default function SeedKeywords() {
return (
<>
<PageMeta title="Seed Keywords" />
<PageMeta title="Seed Keywords" description="Global keyword library for reference" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Seed Keywords</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Global keyword library for reference</p>

View File

@@ -24,6 +24,7 @@ import {
fetchKeywords,
fetchSectorStats,
SectorStats,
SectorStatsItem,
} from '../../services/api';
import Badge from '../../components/ui/badge/Badge';
import { BoltIcon, ShootingStarIcon } from '../../icons';
@@ -32,19 +33,23 @@ import { usePageSizeStore } from '../../store/pageSizeStore';
import { getDifficultyNumber, getDifficultyRange, getDifficultyLabelFromNumber } from '../../utils/difficulty';
import { useSiteStore } from '../../store/siteStore';
import { useSectorStore } from '../../store/sectorStore';
import type { Sector } from '../../store/sectorStore';
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 Input from '../../components/form/input/InputField';
import { Card } from '../../components/ui/card';
import { SectorMetricGrid, StatType } from '../../components/keywords-library/SectorMetricCard';
import SmartSuggestions, { buildSmartSuggestions } from '../../components/keywords-library/SmartSuggestions';
import SectorCardsGrid from '../../components/keywords-library/SectorCardsGrid';
import BulkAddConfirmation from '../../components/keywords-library/BulkAddConfirmation';
export default function IndustriesSectorsKeywords() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const { activeSite } = useSiteStore();
const { activeSector, loadSectorsForSite } = useSectorStore();
const { activeSector, loadSectorsForSite, sectors, setActiveSector } = useSectorStore();
const { pageSize } = usePageSizeStore();
// Data state
@@ -54,9 +59,11 @@ export default function IndustriesSectorsKeywords() {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Track recently added keywords to preserve their state during reload
const recentlyAddedRef = useRef<Set<number>>(new Set());
const attachedSeedKeywordIdsRef = useRef<Set<number>>(new Set());
// Sector Stats state (for metric cards)
const [sectorStats, setSectorStats] = useState<SectorStats | null>(null);
const [sectorStatsList, setSectorStatsList] = useState<SectorStatsItem[]>([]);
const [loadingSectorStats, setLoadingSectorStats] = useState(false);
const [activeStatFilter, setActiveStatFilter] = useState<StatType | null>(null);
@@ -64,6 +71,10 @@ export default function IndustriesSectorsKeywords() {
const [showBulkAddModal, setShowBulkAddModal] = useState(false);
const [bulkAddKeywordIds, setBulkAddKeywordIds] = useState<number[]>([]);
const [bulkAddStatLabel, setBulkAddStatLabel] = useState<string | undefined>();
const [pendingBulkAddKey, setPendingBulkAddKey] = useState<string | null>(null);
const [pendingBulkAddGroup, setPendingBulkAddGroup] = useState<'stat' | 'suggestion' | null>(null);
const [addedStatActions, setAddedStatActions] = useState<Set<string>>(new Set());
const [addedSuggestionActions, setAddedSuggestionActions] = useState<Set<string>>(new Set());
// Ahrefs banner state
const [showAhrefsBanner, setShowAhrefsBanner] = useState(true);
@@ -82,6 +93,8 @@ export default function IndustriesSectorsKeywords() {
const [countryFilter, setCountryFilter] = useState('');
const [difficultyFilter, setDifficultyFilter] = useState('');
const [showNotAddedOnly, setShowNotAddedOnly] = useState(false);
const [volumeMin, setVolumeMin] = useState('');
const [volumeMax, setVolumeMax] = useState('');
// Keyword count tracking
const [addedCount, setAddedCount] = useState(0);
@@ -176,6 +189,7 @@ export default function IndustriesSectorsKeywords() {
const loadSectorStats = useCallback(async () => {
if (!activeSite || !activeSite.industry) {
setSectorStats(null);
setSectorStatsList([]);
return;
}
@@ -183,15 +197,21 @@ export default function IndustriesSectorsKeywords() {
try {
const response = await fetchSectorStats({
industry_id: activeSite.industry,
sector_id: activeSector?.industry_sector ?? undefined,
site_id: activeSite.id,
});
// If sector-specific stats returned
if (response.stats) {
setSectorStats(response.stats as SectorStats);
} else if (response.sectors && response.sectors.length > 0) {
// Aggregate stats from all sectors
const allSectors = response.sectors || [];
setSectorStatsList(allSectors);
if (activeSector?.industry_sector) {
const matching = allSectors.find((sector) => sector.sector_id === activeSector.industry_sector);
if (matching) {
setSectorStats(matching.stats as SectorStats);
return;
}
}
if (allSectors.length > 0) {
const aggregated: SectorStats = {
total: { count: 0 },
available: { count: 0 },
@@ -200,15 +220,14 @@ export default function IndustriesSectorsKeywords() {
long_tail: { count: 0, threshold: 1000 },
quick_wins: { count: 0, threshold: 1000 },
};
response.sectors.forEach((sector) => {
allSectors.forEach((sector) => {
aggregated.total.count += sector.stats.total.count;
aggregated.available.count += sector.stats.available.count;
aggregated.high_volume.count += sector.stats.high_volume.count;
aggregated.premium_traffic.count += sector.stats.premium_traffic.count;
aggregated.long_tail.count += sector.stats.long_tail.count;
aggregated.quick_wins.count += sector.stats.quick_wins.count;
// Use first sector's thresholds (they should be consistent)
if (!aggregated.premium_traffic.threshold) {
aggregated.premium_traffic.threshold = sector.stats.premium_traffic.threshold;
}
@@ -219,8 +238,10 @@ export default function IndustriesSectorsKeywords() {
aggregated.quick_wins.threshold = sector.stats.quick_wins.threshold;
}
});
setSectorStats(aggregated);
} else {
setSectorStats(null);
}
} catch (error) {
console.error('Failed to load sector stats:', error);
@@ -236,6 +257,26 @@ export default function IndustriesSectorsKeywords() {
}
}, [activeSite, activeSector, loadSectorStats]);
// Reset filters and state when site changes
useEffect(() => {
setActiveStatFilter(null);
setSearchTerm('');
setCountryFilter('');
setDifficultyFilter('');
setShowNotAddedOnly(false);
setCurrentPage(1);
setSelectedIds([]);
setAddedStatActions(new Set());
setAddedSuggestionActions(new Set());
}, [activeSite?.id]);
// Reset pagination/selection when sector changes
useEffect(() => {
setActiveStatFilter(null);
setCurrentPage(1);
setSelectedIds([]);
}, [activeSector?.id]);
// Load counts on mount and when site/sector changes
useEffect(() => {
if (activeSite) {
@@ -283,6 +324,9 @@ export default function IndustriesSectorsKeywords() {
console.warn('Could not fetch sectors or attached keywords:', err);
}
// Keep attached IDs available for bulk add actions
attachedSeedKeywordIdsRef.current = attachedSeedKeywordIds;
// Build API filters - use server-side pagination
const pageSizeNum = pageSize || 25;
const filters: any = {
@@ -298,6 +342,8 @@ export default function IndustriesSectorsKeywords() {
if (searchTerm) filters.search = searchTerm;
if (countryFilter) filters.country = countryFilter;
if (volumeMin) filters.volume_min = parseInt(volumeMin);
if (volumeMax) filters.volume_max = parseInt(volumeMax);
// Apply difficulty filter (if API supports it, otherwise we'll filter client-side)
if (difficultyFilter) {
@@ -361,7 +407,7 @@ export default function IndustriesSectorsKeywords() {
setAvailableCount(0);
setShowContent(true);
}
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, showNotAddedOnly, sortBy, sortDirection, toast]);
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, volumeMin, volumeMax, difficultyFilter, showNotAddedOnly, sortBy, sortDirection, toast]);
// Load data when site/sector/filters change (show table by default per plan)
useEffect(() => {
@@ -381,6 +427,7 @@ export default function IndustriesSectorsKeywords() {
// Reset to page 1 on pageSize change
useEffect(() => {
setCurrentPage(1);
setSelectedIds([]);
}, [pageSize]);
// Handle sorting
@@ -388,6 +435,7 @@ export default function IndustriesSectorsKeywords() {
setSortBy(field || 'keyword');
setSortDirection(direction);
setCurrentPage(1);
setSelectedIds([]);
};
// Handle adding keywords to workflow
@@ -684,6 +732,50 @@ export default function IndustriesSectorsKeywords() {
type: 'text' as const,
placeholder: 'Search keywords...',
},
{
key: 'sector',
label: 'Sector',
type: 'select' as const,
options: [
{ value: '', label: 'All Sectors' },
...sectors.map((sector) => ({
value: String(sector.id),
label: sector.name,
})),
],
},
{
key: 'volume',
label: 'Volume',
type: 'custom' as const,
customRender: () => (
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Volume</span>
<Input
type="number"
placeholder="Min"
value={volumeMin}
onChange={(e) => {
setVolumeMin(e.target.value);
setCurrentPage(1);
setSelectedIds([]);
}}
className="w-20 h-8"
/>
<Input
type="number"
placeholder="Max"
value={volumeMax}
onChange={(e) => {
setVolumeMax(e.target.value);
setCurrentPage(1);
setSelectedIds([]);
}}
className="w-20 h-8"
/>
</div>
),
},
{
key: 'country',
label: 'Country',
@@ -730,7 +822,7 @@ export default function IndustriesSectorsKeywords() {
},
],
};
}, [activeSector, handleAddToWorkflow]);
}, [activeSector, handleAddToWorkflow, sectors]);
// Build smart suggestions from sector stats
const smartSuggestions = useMemo(() => {
@@ -738,6 +830,407 @@ export default function IndustriesSectorsKeywords() {
return buildSmartSuggestions(sectorStats, { showOnlyWithResults: true });
}, [sectorStats]);
// Helper: word count for keyword string
const getWordCount = useCallback((keyword: string) => {
return keyword.trim().split(/\s+/).filter(Boolean).length;
}, []);
const buildStatActionKey = useCallback((statType: StatType, count: number) => {
return `${statType}:${count}`;
}, []);
const buildSuggestionActionKey = useCallback((suggestionId: string, count: number) => {
return `${suggestionId}:${count}`;
}, []);
const isStatActionAdded = useCallback((statType: StatType, count: number) => {
return addedStatActions.has(buildStatActionKey(statType, count));
}, [addedStatActions, buildStatActionKey]);
const isSuggestionActionAdded = useCallback((suggestionId: string, count: number) => {
return addedSuggestionActions.has(buildSuggestionActionKey(suggestionId, count));
}, [addedSuggestionActions, buildSuggestionActionKey]);
const fetchBulkKeywords = useCallback(async (options: {
ordering: string;
difficultyMax?: number;
minVolume?: number;
longTail?: boolean;
availableOnly?: boolean;
count: number;
}) => {
if (!activeSite || !activeSector?.industry_sector) {
throw new Error('Please select a site and sector first');
}
const { ordering, difficultyMax, minVolume, longTail, availableOnly, count } = options;
const pageSize = Math.max(500, count * 2);
const filters: any = {
industry: activeSite.industry,
sector: activeSector.industry_sector,
page_size: pageSize,
ordering,
};
if (difficultyMax !== undefined) {
filters.difficulty_max = difficultyMax;
}
const response = await fetchKeywordsLibrary(filters);
let results = response.results || [];
if (minVolume !== undefined) {
results = results.filter((kw) => (kw.volume || 0) >= minVolume);
}
if (longTail) {
results = results.filter((kw) => getWordCount(kw.keyword) >= 4);
}
if (availableOnly) {
results = results.filter((kw) => !attachedSeedKeywordIdsRef.current.has(Number(kw.id)));
}
const topIds = results.slice(0, count).map((kw) => kw.id);
return topIds;
}, [activeSite, activeSector, getWordCount]);
const prepareBulkAdd = useCallback(async (payload: {
label: string;
ids: number[];
actionKey: string;
group: 'stat' | 'suggestion';
}) => {
if (payload.ids.length === 0) {
toast.error('No matching keywords found for this selection');
return;
}
setBulkAddKeywordIds(payload.ids);
setBulkAddStatLabel(payload.label);
setPendingBulkAddKey(payload.actionKey);
setPendingBulkAddGroup(payload.group);
setShowBulkAddModal(true);
}, [toast]);
// Handle stat card click - filters table to show matching keywords
const handleStatClick = useCallback((statType: StatType) => {
// Toggle off if clicking same stat
if (activeStatFilter === statType) {
setActiveStatFilter(null);
setShowNotAddedOnly(false);
setDifficultyFilter('');
setVolumeMin('');
setVolumeMax('');
setSelectedIds([]);
return;
}
setActiveStatFilter(statType);
setCurrentPage(1);
setSelectedIds([]);
const statThresholds = {
highVolume: sectorStats?.high_volume.threshold ?? 10000,
premium: sectorStats?.premium_traffic.threshold ?? 50000,
longTail: sectorStats?.long_tail.threshold ?? 1000,
quickWins: sectorStats?.quick_wins.threshold ?? 1000,
};
switch (statType) {
case 'available':
setShowNotAddedOnly(true);
setDifficultyFilter('');
setVolumeMin('');
setVolumeMax('');
break;
case 'high_volume':
setShowNotAddedOnly(false);
setDifficultyFilter('');
setVolumeMin(String(statThresholds.highVolume));
setVolumeMax('');
setSortBy('volume');
setSortDirection('desc');
break;
case 'premium_traffic':
setShowNotAddedOnly(false);
setDifficultyFilter('');
setVolumeMin(String(statThresholds.premium));
setVolumeMax('');
setSortBy('volume');
setSortDirection('desc');
break;
case 'long_tail':
setShowNotAddedOnly(false);
setDifficultyFilter('');
setVolumeMin(String(statThresholds.longTail));
setVolumeMax('');
setSortBy('keyword');
setSortDirection('asc');
break;
case 'quick_wins':
setShowNotAddedOnly(true);
setDifficultyFilter('1');
setVolumeMin(String(statThresholds.quickWins));
setVolumeMax('');
setSortBy('difficulty');
setSortDirection('asc');
break;
default:
setShowNotAddedOnly(false);
setDifficultyFilter('');
setVolumeMin('');
setVolumeMax('');
}
}, [activeStatFilter, sectorStats]);
const handleSuggestionClick = useCallback((suggestion: any) => {
setActiveStatFilter(null);
setCurrentPage(1);
setSelectedIds([]);
const difficultyMax = suggestion.filterParams?.difficulty_max;
const volumeMinValue = suggestion.filterParams?.volume_min;
const availableOnly = Boolean(suggestion.filterParams?.available_only);
if (availableOnly) {
setShowNotAddedOnly(true);
} else {
setShowNotAddedOnly(false);
}
if (volumeMinValue) {
setVolumeMin(String(volumeMinValue));
} else {
setVolumeMin('');
}
setVolumeMax('');
if (difficultyMax !== undefined) {
if (difficultyMax <= 20) {
setDifficultyFilter('1');
} else if (difficultyMax <= 40) {
setDifficultyFilter('2');
} else {
setDifficultyFilter('');
}
} else {
setDifficultyFilter('');
}
if (suggestion.id === 'quick_wins') {
setSortBy('difficulty');
setSortDirection('asc');
} else {
setSortBy('volume');
setSortDirection('desc');
}
}, []);
// Handle sector card click
const handleSectorSelect = useCallback((sector: Sector | null) => {
setActiveStatFilter(null);
setSelectedIds([]);
setCurrentPage(1);
if (!sector) {
setActiveSector(null);
return;
}
setActiveSector(sector);
}, [setActiveSector]);
// Handle bulk add from metric cards
const handleMetricBulkAdd = useCallback(async (statType: StatType, count: number) => {
if (!activeSite || !activeSector?.industry_sector) {
toast.error('Please select a site and sector first');
return;
}
const actionKey = buildStatActionKey(statType, count);
if (addedStatActions.has(actionKey)) {
return;
}
const statLabelMap: Record<StatType, string> = {
total: 'Total Keywords',
available: 'Available Keywords',
high_volume: 'High Volume',
premium_traffic: 'Premium Traffic',
long_tail: 'Long Tail',
quick_wins: 'Quick Wins',
};
const thresholdMap = {
high_volume: sectorStats?.high_volume.threshold ?? 10000,
premium_traffic: sectorStats?.premium_traffic.threshold ?? 50000,
long_tail: sectorStats?.long_tail.threshold ?? 1000,
quick_wins: sectorStats?.quick_wins.threshold ?? 1000,
};
let ids: number[] = [];
if (statType === 'quick_wins') {
ids = await fetchBulkKeywords({
ordering: 'difficulty',
difficultyMax: 20,
minVolume: thresholdMap.quick_wins,
availableOnly: true,
count,
});
} else if (statType === 'long_tail') {
ids = await fetchBulkKeywords({
ordering: '-volume',
minVolume: thresholdMap.long_tail,
longTail: true,
availableOnly: true,
count,
});
} else if (statType === 'premium_traffic') {
ids = await fetchBulkKeywords({
ordering: '-volume',
minVolume: thresholdMap.premium_traffic,
availableOnly: true,
count,
});
} else if (statType === 'high_volume') {
ids = await fetchBulkKeywords({
ordering: '-volume',
minVolume: thresholdMap.high_volume,
availableOnly: true,
count,
});
} else if (statType === 'available') {
ids = await fetchBulkKeywords({
ordering: '-volume',
availableOnly: true,
count,
});
} else {
ids = await fetchBulkKeywords({
ordering: '-volume',
availableOnly: true,
count,
});
}
await prepareBulkAdd({
label: statLabelMap[statType],
ids,
actionKey,
group: 'stat',
});
}, [activeSite, activeSector, addedStatActions, buildStatActionKey, fetchBulkKeywords, prepareBulkAdd, sectorStats, toast]);
// Handle bulk add from suggestions
const handleSuggestionBulkAdd = useCallback(async (suggestion: any, count: number) => {
if (!activeSite || !activeSector?.industry_sector) {
toast.error('Please select a site and sector first');
return;
}
const actionKey = buildSuggestionActionKey(suggestion.id, count);
if (addedSuggestionActions.has(actionKey)) {
return;
}
const suggestionThresholds = {
quick_wins: sectorStats?.quick_wins.threshold ?? 1000,
long_tail: sectorStats?.long_tail.threshold ?? 1000,
premium_traffic: sectorStats?.premium_traffic.threshold ?? 50000,
};
let ids: number[] = [];
if (suggestion.id === 'quick_wins') {
ids = await fetchBulkKeywords({
ordering: 'difficulty',
difficultyMax: 20,
minVolume: suggestionThresholds.quick_wins,
availableOnly: true,
count,
});
} else if (suggestion.id === 'long_tail') {
ids = await fetchBulkKeywords({
ordering: '-volume',
minVolume: suggestionThresholds.long_tail,
longTail: true,
availableOnly: true,
count,
});
} else if (suggestion.id === 'premium_traffic') {
ids = await fetchBulkKeywords({
ordering: '-volume',
minVolume: suggestionThresholds.premium_traffic,
availableOnly: true,
count,
});
} else if (suggestion.id === 'available') {
ids = await fetchBulkKeywords({
ordering: '-volume',
availableOnly: true,
count,
});
} else {
ids = await fetchBulkKeywords({
ordering: '-volume',
availableOnly: true,
count,
});
}
await prepareBulkAdd({
label: suggestion.label,
ids,
actionKey,
group: 'suggestion',
});
}, [activeSite, activeSector, addedSuggestionActions, buildSuggestionActionKey, fetchBulkKeywords, prepareBulkAdd, sectorStats, toast]);
const handleConfirmBulkAdd = useCallback(async () => {
if (!activeSite || !activeSector) {
throw new Error('Please select a site and sector first');
}
const result = await addSeedKeywordsToWorkflow(
bulkAddKeywordIds,
activeSite.id,
activeSector.id
);
if (!result.success) {
const errorMsg = result.errors?.[0] || 'Unable to add keywords. Please try again.';
throw new Error(errorMsg);
}
if (result.created > 0) {
bulkAddKeywordIds.forEach((id) => recentlyAddedRef.current.add(id));
}
if (pendingBulkAddKey) {
if (pendingBulkAddGroup === 'stat') {
setAddedStatActions((prev) => new Set([...prev, pendingBulkAddKey]));
}
if (pendingBulkAddGroup === 'suggestion') {
setAddedSuggestionActions((prev) => new Set([...prev, pendingBulkAddKey]));
}
}
setPendingBulkAddKey(null);
setPendingBulkAddGroup(null);
setShowBulkAddModal(false);
if (activeSite) {
loadSeedKeywords();
loadKeywordCounts();
loadSectorStats();
}
return {
added: result.created || 0,
skipped: result.skipped || 0,
total_requested: bulkAddKeywordIds.length,
};
}, [activeSite, activeSector, bulkAddKeywordIds, loadKeywordCounts, loadSectorStats, loadSeedKeywords, pendingBulkAddGroup, pendingBulkAddKey]);
// Show WorkflowGuide if no sites
if (sites.length === 0) {
return (
@@ -754,59 +1247,6 @@ export default function IndustriesSectorsKeywords() {
);
}
// Handle stat card click - filters table to show matching keywords
const handleStatClick = (statType: StatType) => {
// Toggle off if clicking same stat
if (activeStatFilter === statType) {
setActiveStatFilter(null);
setShowNotAddedOnly(false);
setDifficultyFilter('');
return;
}
setActiveStatFilter(statType);
setCurrentPage(1);
// Apply filters based on stat type
switch (statType) {
case 'available':
setShowNotAddedOnly(true);
setDifficultyFilter('');
break;
case 'high_volume':
// Volume >= 10K - needs sort by volume desc
setShowNotAddedOnly(false);
setDifficultyFilter('');
setSortBy('volume');
setSortDirection('desc');
break;
case 'premium_traffic':
// Premium traffic - sort by volume
setShowNotAddedOnly(false);
setDifficultyFilter('');
setSortBy('volume');
setSortDirection('desc');
break;
case 'long_tail':
// Long tail - can't filter by word count server-side, just show all sorted
setShowNotAddedOnly(false);
setDifficultyFilter('');
setSortBy('keyword');
setSortDirection('asc');
break;
case 'quick_wins':
// Quick wins - low difficulty + available
setShowNotAddedOnly(true);
setDifficultyFilter('1'); // Very Easy (level 1 = backend 0-20)
setSortBy('difficulty');
setSortDirection('asc');
break;
default:
setShowNotAddedOnly(false);
setDifficultyFilter('');
}
};
return (
<>
<PageMeta title="Keywords Library" description="Browse curated keywords and add them to your workflow. Ahrefs research coming soon." />
@@ -816,66 +1256,49 @@ export default function IndustriesSectorsKeywords() {
badge={{ icon: <BoltIcon />, color: 'blue' }}
/>
{/* Sector Cards - Top of page */}
{activeSite && sectors.length > 0 && (
<div className="mx-6 mt-6">
<SectorCardsGrid
sectors={sectors}
sectorStats={sectorStatsList}
activeSectorId={activeSector?.id}
onSelectSector={handleSectorSelect}
isLoading={loadingSectorStats}
/>
</div>
)}
{/* Sector Metric Cards - Show when site is selected */}
{activeSite && (
<div className="mx-6 mt-6">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
Keyword Stats {activeSector ? `${activeSector.name}` : '— All Sectors'}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Click a card to filter the table below
</p>
</div>
<SectorMetricGrid
stats={sectorStats}
activeStatType={activeStatFilter}
onStatClick={handleStatClick}
onBulkAdd={activeSector ? handleMetricBulkAdd : undefined}
isAddedAction={isStatActionAdded}
clickable={true}
sectorName={activeSector?.name}
isLoading={loadingSectorStats}
/>
</div>
)}
{/* Smart Suggestions Panel */}
{activeSite && sectorStats && smartSuggestions.length > 0 && (
{/* Smart Suggestions Panel - Always show when site is selected */}
{activeSite && sectorStats && (
<div className="mx-6 mt-6">
<SmartSuggestions
suggestions={smartSuggestions}
onSuggestionClick={(suggestion) => {
// Apply the filter from the suggestion
if (suggestion.filterParams.statType) {
handleStatClick(suggestion.filterParams.statType as StatType);
}
}}
onSuggestionClick={handleSuggestionClick}
onBulkAdd={activeSector ? handleSuggestionBulkAdd : undefined}
isAddedAction={isSuggestionActionAdded}
enableFilterClick={true}
isLoading={loadingSectorStats}
/>
</div>
)}
{/* Show info banner when no sector is selected */}
{!activeSector && activeSite && (
<div className="mx-6 mt-6 mb-4">
<div className="bg-brand-50 dark:bg-brand-900/20 border border-brand-200 dark:border-brand-800 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-brand-600 dark:text-brand-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-brand-900 dark:text-brand-200">
Choose a Topic Area First
</h3>
<p className="mt-1 text-sm text-brand-700 dark:text-brand-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>
)}
{/* Keywords Table - Shown by default (per plan) */}
{activeSite && (
<TablePageTemplate
@@ -883,9 +1306,14 @@ export default function IndustriesSectorsKeywords() {
data={seedKeywords}
loading={!showContent}
showContent={showContent}
defaultShowFilters={true}
centerFilters={true}
filters={pageConfig.filters}
filterValues={{
sector: activeSector?.id ? String(activeSector.id) : '',
search: searchTerm,
volume_min: volumeMin,
volume_max: volumeMax,
country: countryFilter,
difficulty: difficultyFilter,
showNotAddedOnly: showNotAddedOnly ? 'true' : '',
@@ -921,19 +1349,49 @@ export default function IndustriesSectorsKeywords() {
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (activeStatFilter) {
setActiveStatFilter(null);
}
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'sector') {
if (!stringValue) {
handleSectorSelect(null);
} else {
const selectedSector = sectors.find((sector) => String(sector.id) === stringValue) || null;
handleSectorSelect(selectedSector);
}
} else if (key === 'country') {
setCountryFilter(stringValue);
setCurrentPage(1);
setSelectedIds([]);
} else if (key === 'difficulty') {
setDifficultyFilter(stringValue);
setCurrentPage(1);
setSelectedIds([]);
} else if (key === 'showNotAddedOnly') {
setShowNotAddedOnly(stringValue === 'true');
setCurrentPage(1);
setSelectedIds([]);
}
}}
onFilterReset={() => {
// Clear all filters
setSearchTerm('');
setCountryFilter('');
setVolumeMin('');
setVolumeMax('');
setDifficultyFilter('');
setShowNotAddedOnly(false);
setActiveStatFilter(null);
setCurrentPage(1);
setSelectedIds([]);
setActiveSector(null);
// Reset sorting to default
setSortBy('keyword');
setSortDirection('asc');
}}
onBulkAction={async (actionKey: string, ids: string[]) => {
if (actionKey === 'add_selected_to_workflow') {
await handleBulkAddSelected(ids);
@@ -1045,6 +1503,22 @@ export default function IndustriesSectorsKeywords() {
</div>
</div>
</Modal>
{/* Bulk Add Confirmation */}
<BulkAddConfirmation
isOpen={showBulkAddModal}
onClose={() => {
setShowBulkAddModal(false);
setBulkAddKeywordIds([]);
setBulkAddStatLabel(undefined);
setPendingBulkAddKey(null);
setPendingBulkAddGroup(null);
}}
onConfirm={handleConfirmBulkAdd}
keywordCount={bulkAddKeywordIds.length}
sectorName={activeSector?.name}
statTypeLabel={bulkAddStatLabel}
/>
</>
);
}