fixes ofkewyrod library
This commit is contained in:
@@ -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<Site[]>([]);
|
||||
@@ -87,16 +65,6 @@ export default function IndustriesSectorsKeywords() {
|
||||
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);
|
||||
const [highOpportunityLoaded, setHighOpportunityLoaded] = useState(false);
|
||||
const [sectorKeywordData, setSectorKeywordData] = useState<SectorKeywordData[]>([]);
|
||||
const [addingOption, setAddingOption] = useState<string | null>(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<number>();
|
||||
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<number>();
|
||||
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 (
|
||||
<div className="mx-6 mt-6 mb-6">
|
||||
<Card className="p-4 bg-success-50 dark:bg-success-900/20 border-success-200 dark:border-success-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-success-900 dark:text-success-200">
|
||||
Quick-Start Keywords Added Successfully
|
||||
</h3>
|
||||
<p className="text-xs text-success-700 dark:text-success-300">
|
||||
{addedCount} keywords added to your workflow
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="success"
|
||||
size="sm"
|
||||
onClick={() => setShowHighOpportunity(true)}
|
||||
>
|
||||
Show Section
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-6 mt-6 mb-6 p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Quick-Start Keywords — Complimentary
|
||||
</h2>
|
||||
<Badge tone="brand" variant="soft" size="sm">
|
||||
<BoltIcon className="w-3 h-3 mr-1" />
|
||||
Pre-Vetted
|
||||
</Badge>
|
||||
</div>
|
||||
{!allAdded && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowHighOpportunity(false)}
|
||||
>
|
||||
Hide
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Ready-to-use keywords to jumpstart your content — no research needed. Simply add to your workflow and start creating.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loading State */}
|
||||
{loadingOpportunityKeywords ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Sector Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6 items-start">
|
||||
{sectorKeywordData.map((sector) => (
|
||||
<div key={sector.sectorSlug} className="flex flex-col gap-3">
|
||||
{/* Sector Name */}
|
||||
<h4 className="text-base font-semibold text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
{sector.sectorName}
|
||||
</h4>
|
||||
|
||||
{/* Options Cards */}
|
||||
{sector.options.map((option) => {
|
||||
const addingKey = `${sector.sectorSlug}-${option.type}`;
|
||||
const isAdding = addingOption === addingKey;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={option.type}
|
||||
className={`p-4 transition-all flex flex-col ${
|
||||
option.added
|
||||
? 'border-success-300 dark:border-success-700 bg-success-50 dark:bg-success-900/20'
|
||||
: 'hover:border-brand-300 dark:hover:border-brand-700'
|
||||
}`}
|
||||
>
|
||||
{/* Option Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{option.label}
|
||||
</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{option.keywordCount} keywords
|
||||
</p>
|
||||
</div>
|
||||
{option.added ? (
|
||||
<Badge tone="success" variant="soft" size="sm">
|
||||
<CheckCircleIcon className="w-3 h-3 mr-1" />
|
||||
Added
|
||||
</Badge>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
handleAddSectorKeywords(sector.sectorSlug, option.type)
|
||||
}
|
||||
disabled={isAdding}
|
||||
>
|
||||
{isAdding ? 'Adding...' : 'Add All'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sample Keywords */}
|
||||
<div className="flex flex-wrap gap-1.5 flex-1">
|
||||
{option.keywords.slice(0, 3).map((kw) => (
|
||||
<Badge
|
||||
key={kw.id}
|
||||
tone={option.added ? 'success' : 'neutral'}
|
||||
variant="soft"
|
||||
size="xs"
|
||||
className="text-xs"
|
||||
>
|
||||
{kw.keyword}
|
||||
</Badge>
|
||||
))}
|
||||
{option.keywordCount > 3 && (
|
||||
<Badge
|
||||
tone="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className="text-xs"
|
||||
>
|
||||
+{option.keywordCount - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Success Summary */}
|
||||
{addedCount > 0 && (
|
||||
<Card className="p-4 bg-success-50 dark:bg-success-900/20 border-success-200 dark:border-success-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
||||
<span className="text-sm text-success-700 dark:text-success-300">
|
||||
{addedCount} keywords added to your workflow
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<PageMeta title="Keywords Library" description="Browse and add keywords to your workflow" />
|
||||
@@ -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 (
|
||||
<>
|
||||
<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="Keywords Library"
|
||||
badge={{ icon: <BoltIcon />, color: 'blue' }}
|
||||
@@ -1317,41 +852,9 @@ export default function IndustriesSectorsKeywords() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* High Opportunity Keywords Section - Loads First */}
|
||||
<HighOpportunityKeywordsSection />
|
||||
|
||||
{/* Browse Individual Keywords Section - Shows after High Opportunity is loaded */}
|
||||
{highOpportunityLoaded && !showBrowseTable && activeSite && (
|
||||
<div className="mx-6 mt-6 mb-6">
|
||||
<Card className="p-6">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-3">
|
||||
<DocsIcon className="w-6 h-6 text-gray-400" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Browse Individual Keywords
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-2xl mx-auto">
|
||||
💡 <strong>Recommended:</strong> 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.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="brand"
|
||||
size="md"
|
||||
onClick={() => setShowBrowseTable(true)}
|
||||
startIcon={<DocsIcon className="w-4 h-4" />}
|
||||
>
|
||||
Browse Full Keyword Library ({availableCount.toLocaleString()} available)
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
<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">
|
||||
@@ -1373,8 +876,8 @@ export default function IndustriesSectorsKeywords() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keywords Browse Table - Only show when user clicks browse button */}
|
||||
{showBrowseTable && (
|
||||
{/* Keywords Table - Shown by default (per plan) */}
|
||||
{activeSite && (
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={seedKeywords}
|
||||
@@ -1457,8 +960,8 @@ export default function IndustriesSectorsKeywords() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Ahrefs Coming Soon Banner - Shows at bottom after High Opportunity is loaded */}
|
||||
{highOpportunityLoaded && showAhrefsBanner && (
|
||||
{/* Ahrefs Coming Soon Banner */}
|
||||
{showAhrefsBanner && (
|
||||
<div className="mx-6 mt-6 mb-6">
|
||||
<Card className="p-4 bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-3">
|
||||
|
||||
Reference in New Issue
Block a user