keywrods libarry update
This commit is contained in:
@@ -186,7 +186,7 @@ export default function Help() {
|
||||
{ id: "dashboard", title: "Dashboard", level: 1 },
|
||||
{ id: "setup", title: "Setup & Onboarding", level: 1 },
|
||||
{ id: "onboarding-wizard", title: "Onboarding Wizard", level: 2 },
|
||||
{ id: "add-keywords", title: "Add Keywords", level: 2 },
|
||||
{ id: "keywords-library", title: "Keywords Library", level: 2 },
|
||||
{ id: "content-settings", title: "Content Settings", level: 2 },
|
||||
{ id: "sites", title: "Sites Management", level: 2 },
|
||||
{ id: "planner-module", title: "Planner Module", level: 1 },
|
||||
@@ -214,7 +214,7 @@ export default function Help() {
|
||||
const faqItems = [
|
||||
{
|
||||
question: "How do I add keywords to my workflow?",
|
||||
answer: "Navigate to Setup → Add Keywords or Planner → Keyword Opportunities. Browse available keywords, use filters to find relevant ones, and click 'Add to Workflow' on individual keywords or use bulk selection to add multiple keywords at once. You can also import keywords via CSV upload."
|
||||
answer: "Navigate to Keywords Library in the sidebar. Browse available keywords by industry and sector, use filters to find relevant ones, and click 'Add to Workflow' on individual keywords or use bulk selection to add multiple keywords at once. You can also import keywords via CSV upload."
|
||||
},
|
||||
{
|
||||
question: "What is the difference between Keywords and Clusters?",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Keyword Library Page (formerly Add Keywords)
|
||||
* Keywords Library Page (formerly Add Keywords / Seed 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
|
||||
* Features: Sector Metric Cards, Smart Suggestions, Browse all keywords, CSV import
|
||||
* Coming soon: Ahrefs keyword research integration (March/April 2026)
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { usePageLoading } from '../../context/PageLoadingContext';
|
||||
import WorkflowGuide from '../../components/onboarding/WorkflowGuide';
|
||||
import {
|
||||
fetchSeedKeywords,
|
||||
fetchKeywordsLibrary,
|
||||
SeedKeyword,
|
||||
SeedKeywordResponse,
|
||||
fetchSites,
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
fetchSiteSectors,
|
||||
fetchIndustries,
|
||||
fetchKeywords,
|
||||
fetchSectorStats,
|
||||
SectorStats,
|
||||
bulkAddKeywordsToSite,
|
||||
} from '../../services/api';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { BoltIcon, PlusIcon, CheckCircleIcon, ShootingStarIcon, DocsIcon } from '../../icons';
|
||||
@@ -38,6 +41,9 @@ 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 {
|
||||
@@ -71,6 +77,16 @@ export default function IndustriesSectorsKeywords() {
|
||||
// Track recently added keywords to preserve their state during reload
|
||||
const recentlyAddedRef = useRef<Set<number>>(new Set());
|
||||
|
||||
// Sector Stats state (for metric cards)
|
||||
const [sectorStats, setSectorStats] = useState<SectorStats | null>(null);
|
||||
const [loadingSectorStats, setLoadingSectorStats] = useState(false);
|
||||
const [activeStatFilter, setActiveStatFilter] = useState<StatType | null>(null);
|
||||
|
||||
// Bulk Add confirmation modal state
|
||||
const [showBulkAddModal, setShowBulkAddModal] = useState(false);
|
||||
const [bulkAddKeywordIds, setBulkAddKeywordIds] = useState<number[]>([]);
|
||||
const [bulkAddStatLabel, setBulkAddStatLabel] = useState<string | undefined>();
|
||||
|
||||
// High Opportunity Keywords state
|
||||
const [showHighOpportunity, setShowHighOpportunity] = useState(true);
|
||||
const [loadingOpportunityKeywords, setLoadingOpportunityKeywords] = useState(false);
|
||||
@@ -186,7 +202,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
if (!siteSector.is_active) continue;
|
||||
|
||||
// Fetch all keywords for this sector
|
||||
const response = await fetchSeedKeywords({
|
||||
const response = await fetchKeywordsLibrary({
|
||||
industry: industry.id,
|
||||
sector: siteSector.industry_sector,
|
||||
page_size: 500,
|
||||
@@ -301,7 +317,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
filters.sector = activeSector.industry_sector;
|
||||
}
|
||||
|
||||
const data = await fetchSeedKeywords(filters);
|
||||
const data = await fetchKeywordsLibrary(filters);
|
||||
const totalAvailable = data.count || 0;
|
||||
|
||||
setAddedCount(totalAdded);
|
||||
@@ -311,6 +327,70 @@ export default function IndustriesSectorsKeywords() {
|
||||
}
|
||||
}, [activeSite, activeSector]);
|
||||
|
||||
// Load sector stats for metric cards
|
||||
const loadSectorStats = useCallback(async () => {
|
||||
if (!activeSite || !activeSite.industry) {
|
||||
setSectorStats(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingSectorStats(true);
|
||||
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 aggregated: SectorStats = {
|
||||
total: { count: 0 },
|
||||
available: { count: 0 },
|
||||
high_volume: { count: 0, threshold: 10000 },
|
||||
premium_traffic: { count: 0, threshold: 50000 },
|
||||
long_tail: { count: 0, threshold: 1000 },
|
||||
quick_wins: { count: 0, threshold: 1000 },
|
||||
};
|
||||
|
||||
response.sectors.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;
|
||||
}
|
||||
if (!aggregated.long_tail.threshold) {
|
||||
aggregated.long_tail.threshold = sector.stats.long_tail.threshold;
|
||||
}
|
||||
if (!aggregated.quick_wins.threshold) {
|
||||
aggregated.quick_wins.threshold = sector.stats.quick_wins.threshold;
|
||||
}
|
||||
});
|
||||
|
||||
setSectorStats(aggregated);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load sector stats:', error);
|
||||
} finally {
|
||||
setLoadingSectorStats(false);
|
||||
}
|
||||
}, [activeSite, activeSector]);
|
||||
|
||||
// Load sector stats when site/sector changes
|
||||
useEffect(() => {
|
||||
if (activeSite) {
|
||||
loadSectorStats();
|
||||
}
|
||||
}, [activeSite, activeSector, loadSectorStats]);
|
||||
|
||||
// Load counts on mount and when site/sector changes
|
||||
useEffect(() => {
|
||||
if (activeSite) {
|
||||
@@ -394,7 +474,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
}
|
||||
|
||||
// Fetch only current page from API
|
||||
const data: SeedKeywordResponse = await fetchSeedKeywords(filters);
|
||||
const data: SeedKeywordResponse = await fetchKeywordsLibrary(filters);
|
||||
|
||||
// Mark already-attached keywords
|
||||
const results = (data.results || []).map(sk => {
|
||||
@@ -510,7 +590,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
}
|
||||
|
||||
// Show skipped count if any
|
||||
if (result.skipped > 0) {
|
||||
if (result.skipped && result.skipped > 0) {
|
||||
toast.warning(`${result.skipped} keyword(s) were skipped (already exist or validation failed)`);
|
||||
}
|
||||
|
||||
@@ -849,7 +929,6 @@ export default function IndustriesSectorsKeywords() {
|
||||
handleAddToWorkflow([row.id]);
|
||||
}
|
||||
}}
|
||||
title={!activeSector ? 'Please select a sector first' : ''}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
@@ -1116,9 +1195,9 @@ export default function IndustriesSectorsKeywords() {
|
||||
if (highOpportunityLoaded && sites.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Keyword Library" description="Browse and add keywords to your workflow" />
|
||||
<PageMeta title="Keywords Library" description="Browse and add keywords to your workflow" />
|
||||
<PageHeader
|
||||
title="Keyword Library"
|
||||
title="Keywords Library"
|
||||
badge={{ icon: <BoltIcon />, color: 'blue' }}
|
||||
/>
|
||||
<div className="p-6">
|
||||
@@ -1128,14 +1207,117 @@ 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);
|
||||
setShowBrowseTable(true);
|
||||
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('');
|
||||
}
|
||||
};
|
||||
|
||||
// Build smart suggestions from sector stats
|
||||
const smartSuggestions = useMemo(() => {
|
||||
if (!sectorStats) return [];
|
||||
return buildSmartSuggestions(sectorStats, { showOnlyWithResults: true });
|
||||
}, [sectorStats]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Keyword Library" description="Browse curated keywords and add them to your workflow. Ahrefs research coming soon." />
|
||||
<PageMeta title="Keywords Library" description="Browse curated keywords and add them to your workflow. Ahrefs research coming soon." />
|
||||
|
||||
{/* TEST TEXT - REMOVE AFTER VERIFICATION */}
|
||||
<div className="text-red-500 text-2xl font-bold p-4 text-center">
|
||||
Test Text
|
||||
</div>
|
||||
|
||||
<PageHeader
|
||||
title="Keyword Library"
|
||||
title="Keywords Library"
|
||||
badge={{ icon: <BoltIcon />, color: 'blue' }}
|
||||
/>
|
||||
|
||||
{/* 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}
|
||||
sectorName={activeSector?.name}
|
||||
isLoading={loadingSectorStats}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Smart Suggestions Panel */}
|
||||
{activeSite && sectorStats && smartSuggestions.length > 0 && (
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
isLoading={loadingSectorStats}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* High Opportunity Keywords Section - Loads First */}
|
||||
<HighOpportunityKeywordsSection />
|
||||
|
||||
@@ -1218,7 +1400,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
<>
|
||||
<div className="w-px h-4 bg-gray-300 dark:bg-gray-600" />
|
||||
<Button
|
||||
variant="success"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => navigate('/planner/keywords')}
|
||||
endIcon={
|
||||
|
||||
Reference in New Issue
Block a user