keywrods library fixes

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-18 20:55:02 +00:00
parent 4cf27fa875
commit 05bc433c80
8 changed files with 435 additions and 520 deletions

View File

@@ -25,6 +25,8 @@ import {
fetchSectorStats,
SectorStats,
SectorStatsItem,
fetchKeywordsLibraryFilterOptions,
FilterOption,
} from '../../services/api';
import Badge from '../../components/ui/badge/Badge';
import { BoltIcon, ShootingStarIcon } from '../../icons';
@@ -41,7 +43,7 @@ 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 SmartSuggestions from '../../components/keywords-library/SmartSuggestions';
import SectorCardsGrid from '../../components/keywords-library/SectorCardsGrid';
import BulkAddConfirmation from '../../components/keywords-library/BulkAddConfirmation';
@@ -72,9 +74,7 @@ export default function IndustriesSectorsKeywords() {
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);
@@ -95,6 +95,10 @@ export default function IndustriesSectorsKeywords() {
const [showNotAddedOnly, setShowNotAddedOnly] = useState(false);
const [volumeMin, setVolumeMin] = useState('');
const [volumeMax, setVolumeMax] = useState('');
// Dynamic filter options (cascading)
const [countryOptions, setCountryOptions] = useState<FilterOption[] | undefined>(undefined);
const [difficultyOptions, setDifficultyOptions] = useState<FilterOption[] | undefined>(undefined);
// Keyword count tracking
const [addedCount, setAddedCount] = useState(0);
@@ -267,7 +271,6 @@ export default function IndustriesSectorsKeywords() {
setCurrentPage(1);
setSelectedIds([]);
setAddedStatActions(new Set());
setAddedSuggestionActions(new Set());
}, [activeSite?.id]);
// Reset pagination/selection when sector changes
@@ -275,8 +278,44 @@ export default function IndustriesSectorsKeywords() {
setActiveStatFilter(null);
setCurrentPage(1);
setSelectedIds([]);
setAddedStatActions(new Set());
}, [activeSector?.id]);
// Load cascading filter options based on current filters
const loadFilterOptions = useCallback(async () => {
if (!activeSite?.industry) return;
const difficultyLabel = difficultyFilter ? getDifficultyLabelFromNumber(parseInt(difficultyFilter, 10)) : null;
const difficultyRange = difficultyLabel ? getDifficultyRange(difficultyLabel) : null;
try {
const options = await fetchKeywordsLibraryFilterOptions({
industry_id: activeSite.industry,
sector_id: activeSector?.industry_sector || undefined,
country: countryFilter || undefined,
difficulty_min: difficultyRange?.min,
difficulty_max: difficultyRange?.max,
volume_min: volumeMin ? Number(volumeMin) : undefined,
volume_max: volumeMax ? Number(volumeMax) : undefined,
search: searchTerm || undefined,
});
setCountryOptions(options.countries || []);
setDifficultyOptions(
(options.difficulty?.levels || []).map((level) => ({
value: String(level.level),
label: `${level.level} - ${level.label}`,
}))
);
} catch (error) {
console.error('Failed to load filter options:', error);
}
}, [activeSite?.industry, activeSector?.industry_sector, countryFilter, difficultyFilter, volumeMin, volumeMax, searchTerm]);
useEffect(() => {
loadFilterOptions();
}, [loadFilterOptions]);
// Load counts on mount and when site/sector changes
useEffect(() => {
if (activeSite) {
@@ -297,31 +336,39 @@ export default function IndustriesSectorsKeywords() {
setShowContent(false);
try {
// Get already-attached keywords for marking (lightweight check)
// Get already-attached keywords for marking (paginate to ensure persistence)
const attachedSeedKeywordIds = new Set<number>();
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);
const fetchAttachedSeedKeywordIds = async (siteId: number, sectorId?: number) => {
const pageSize = 500;
let page = 1;
while (true) {
const keywordsData = await fetchKeywords({
site_id: siteId,
sector_id: sectorId,
page,
page_size: pageSize,
});
(keywordsData.results || []).forEach((k: any) => {
const seedKeywordId = k.seed_keyword_id || (k.seed_keyword && k.seed_keyword.id);
if (seedKeywordId) {
attachedSeedKeywordIds.add(Number(seedKeywordId));
}
});
if (!keywordsData.next || (keywordsData.results || []).length < pageSize) {
break;
}
page += 1;
}
};
try {
if (activeSector?.id) {
await fetchAttachedSeedKeywordIds(activeSite.id, activeSector.id);
} else {
await fetchAttachedSeedKeywordIds(activeSite.id);
}
} catch (err) {
console.warn('Could not fetch sectors or attached keywords:', err);
console.warn('Could not fetch attached keywords:', err);
}
// Keep attached IDs available for bulk add actions
@@ -342,8 +389,24 @@ 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);
if (volumeMin !== '') {
const parsed = Number(volumeMin);
if (!Number.isNaN(parsed)) {
filters.volume_min = parsed;
}
}
if (volumeMax !== '') {
const parsed = Number(volumeMax);
if (!Number.isNaN(parsed)) {
filters.volume_max = parsed;
}
}
const useServerAvailableFilter = Boolean(showNotAddedOnly && activeSite?.id);
if (useServerAvailableFilter) {
filters.site_id = activeSite.id;
filters.available_only = true;
}
// Apply difficulty filter (if API supports it, otherwise we'll filter client-side)
if (difficultyFilter) {
@@ -386,15 +449,19 @@ export default function IndustriesSectorsKeywords() {
setAddedCount(totalAdded);
setAvailableCount(actualAvailable);
// Apply "not yet added" filter client-side (if API doesn't support it)
// Apply "not yet added" filter client-side only when server filtering isn't used
let filteredResults = results;
if (showNotAddedOnly) {
if (showNotAddedOnly && !useServerAvailableFilter) {
filteredResults = results.filter(sk => !sk.isAdded);
}
const effectiveTotalCount = useServerAvailableFilter
? apiTotalCount
: (showNotAddedOnly && activeSite ? Math.max(apiTotalCount - attachedSeedKeywordIds.size, 0) : apiTotalCount);
setSeedKeywords(filteredResults);
setTotalCount(apiTotalCount);
setTotalPages(Math.ceil(apiTotalCount / pageSizeNum));
setTotalCount(effectiveTotalCount);
setTotalPages(Math.ceil(effectiveTotalCount / pageSizeNum));
setShowContent(true);
} catch (error: any) {
@@ -409,6 +476,42 @@ export default function IndustriesSectorsKeywords() {
}
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, volumeMin, volumeMax, difficultyFilter, showNotAddedOnly, sortBy, sortDirection, toast]);
const getAddedStatStorageKey = useCallback(() => {
if (!activeSite?.id) return null;
const sectorKey = activeSector?.id ? `sector-${activeSector.id}` : 'sector-all';
return `keywordsLibrary:addedStatActions:${activeSite.id}:${sectorKey}`;
}, [activeSite?.id, activeSector?.id]);
useEffect(() => {
const storageKey = getAddedStatStorageKey();
if (!storageKey) {
setAddedStatActions(new Set());
return;
}
try {
const raw = localStorage.getItem(storageKey);
if (raw) {
const parsed = JSON.parse(raw) as string[];
setAddedStatActions(new Set(parsed));
} else {
setAddedStatActions(new Set());
}
} catch (error) {
console.warn('Failed to load added stat actions:', error);
setAddedStatActions(new Set());
}
}, [getAddedStatStorageKey]);
useEffect(() => {
const storageKey = getAddedStatStorageKey();
if (!storageKey) return;
try {
localStorage.setItem(storageKey, JSON.stringify(Array.from(addedStatActions)));
} catch (error) {
console.warn('Failed to persist added stat actions:', error);
}
}, [addedStatActions, getAddedStatStorageKey]);
// Load data when site/sector/filters change (show table by default per plan)
useEffect(() => {
if (activeSite) {
@@ -621,6 +724,32 @@ export default function IndustriesSectorsKeywords() {
const pageConfig = useMemo(() => {
const showSectorColumn = !activeSector;
const defaultCountryOptions: FilterOption[] = [
{ 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' },
];
const countryFilterOptions = countryOptions && countryOptions.length > 0
? countryOptions
: defaultCountryOptions;
const defaultDifficultyOptions: FilterOption[] = [
{ 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' },
];
const difficultyFilterOptions = difficultyOptions && difficultyOptions.length > 0
? difficultyOptions
: defaultDifficultyOptions;
return {
columns: [
{
@@ -782,13 +911,7 @@ export default function IndustriesSectorsKeywords() {
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' },
...countryFilterOptions,
],
},
{
@@ -797,11 +920,7 @@ export default function IndustriesSectorsKeywords() {
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' },
...difficultyFilterOptions,
],
},
{
@@ -822,13 +941,7 @@ export default function IndustriesSectorsKeywords() {
},
],
};
}, [activeSector, handleAddToWorkflow, sectors]);
// Build smart suggestions from sector stats
const smartSuggestions = useMemo(() => {
if (!sectorStats) return [];
return buildSmartSuggestions(sectorStats, { showOnlyWithResults: true });
}, [sectorStats]);
}, [activeSector, handleAddToWorkflow, sectors, countryOptions, difficultyOptions, volumeMin, volumeMax]);
// Helper: word count for keyword string
const getWordCount = useCallback((keyword: string) => {
@@ -839,18 +952,10 @@ export default function IndustriesSectorsKeywords() {
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;
@@ -900,7 +1005,6 @@ export default function IndustriesSectorsKeywords() {
label: string;
ids: number[];
actionKey: string;
group: 'stat' | 'suggestion';
}) => {
if (payload.ids.length === 0) {
toast.error('No matching keywords found for this selection');
@@ -910,7 +1014,6 @@ export default function IndustriesSectorsKeywords() {
setBulkAddKeywordIds(payload.ids);
setBulkAddStatLabel(payload.label);
setPendingBulkAddKey(payload.actionKey);
setPendingBulkAddGroup(payload.group);
setShowBulkAddModal(true);
}, [toast]);
@@ -985,48 +1088,6 @@ export default function IndustriesSectorsKeywords() {
}
}, [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) => {
@@ -1117,74 +1178,9 @@ export default function IndustriesSectorsKeywords() {
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');
@@ -1206,16 +1202,10 @@ export default function IndustriesSectorsKeywords() {
}
if (pendingBulkAddKey) {
if (pendingBulkAddGroup === 'stat') {
setAddedStatActions((prev) => new Set([...prev, pendingBulkAddKey]));
}
if (pendingBulkAddGroup === 'suggestion') {
setAddedSuggestionActions((prev) => new Set([...prev, pendingBulkAddKey]));
}
setAddedStatActions((prev) => new Set([...prev, pendingBulkAddKey]));
}
setPendingBulkAddKey(null);
setPendingBulkAddGroup(null);
setShowBulkAddModal(false);
if (activeSite) {
@@ -1229,7 +1219,7 @@ export default function IndustriesSectorsKeywords() {
skipped: result.skipped || 0,
total_requested: bulkAddKeywordIds.length,
};
}, [activeSite, activeSector, bulkAddKeywordIds, loadKeywordCounts, loadSectorStats, loadSeedKeywords, pendingBulkAddGroup, pendingBulkAddKey]);
}, [activeSite, activeSector, bulkAddKeywordIds, loadKeywordCounts, loadSectorStats, loadSeedKeywords, pendingBulkAddKey]);
// Show WorkflowGuide if no sites
if (sites.length === 0) {
@@ -1269,33 +1259,21 @@ export default function IndustriesSectorsKeywords() {
</div>
)}
{/* Sector Metric Cards - Show when site is selected */}
{activeSite && (
<div className="mx-6 mt-6">
<SectorMetricGrid
stats={sectorStats}
activeStatType={activeStatFilter}
onStatClick={handleStatClick}
onBulkAdd={activeSector ? handleMetricBulkAdd : undefined}
isAddedAction={isStatActionAdded}
clickable={true}
sectorName={activeSector?.name}
isLoading={loadingSectorStats}
/>
</div>
)}
{/* Smart Suggestions Panel - Always show when site is selected */}
{activeSite && sectorStats && (
<div className="mx-6 mt-6">
<SmartSuggestions
suggestions={smartSuggestions}
onSuggestionClick={handleSuggestionClick}
onBulkAdd={activeSector ? handleSuggestionBulkAdd : undefined}
isAddedAction={isSuggestionActionAdded}
enableFilterClick={true}
isLoading={loadingSectorStats}
/>
<SmartSuggestions isLoading={loadingSectorStats}>
<SectorMetricGrid
stats={sectorStats}
activeStatType={activeStatFilter}
onStatClick={handleStatClick}
onBulkAdd={activeSector ? handleMetricBulkAdd : undefined}
isAddedAction={isStatActionAdded}
clickable={true}
sectorName={activeSector?.name}
isLoading={false}
/>
</SmartSuggestions>
</div>
)}
@@ -1512,7 +1490,6 @@ export default function IndustriesSectorsKeywords() {
setBulkAddKeywordIds([]);
setBulkAddStatLabel(undefined);
setPendingBulkAddKey(null);
setPendingBulkAddGroup(null);
}}
onConfirm={handleConfirmBulkAdd}
keywordCount={bulkAddKeywordIds.length}