more impeorventes for kewyrods libreary
This commit is contained in:
160
frontend/src/components/dashboard/KeywordLibraryStatsWidget.tsx
Normal file
160
frontend/src/components/dashboard/KeywordLibraryStatsWidget.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Keyword Library Stats Widget
|
||||
* Shows global seed keyword statistics: industries, countries, and totals
|
||||
*/
|
||||
|
||||
import { Card } from '../ui/card';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import { Spinner } from '../ui/spinner/Spinner';
|
||||
import { DocsIcon, GridIcon, GlobeIcon } from '../../icons';
|
||||
import { KeywordStats } from '../../services/api';
|
||||
|
||||
interface KeywordLibraryStatsWidgetProps {
|
||||
stats: KeywordStats | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default function KeywordLibraryStatsWidget({ stats, loading }: KeywordLibraryStatsWidgetProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{[1, 2, 3].map(i => (
|
||||
<Card key={i} className="p-6">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner size="md" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{/* Card 1: Top Industries */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-brand-100 dark:bg-brand-900/50 flex items-center justify-center">
|
||||
<GridIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Starter Keywords
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
By Industry
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{stats.industries.slice(0, 7).map((industry) => (
|
||||
<div key={industry.slug} className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{industry.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Vol: {industry.total_volume.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Badge tone="brand" variant="soft" size="sm">
|
||||
{industry.keyword_count.toLocaleString()}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{stats.industries.length > 7 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
+{stats.industries.length - 7} more industries
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Card 2: By Country */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
|
||||
<GlobeIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
By Country
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Geographic coverage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{stats.countries.map((country) => (
|
||||
<div key={country.country} className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{country.country_display}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Vol: {country.total_volume.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Badge tone="purple" variant="soft" size="sm">
|
||||
{country.keyword_count.toLocaleString()}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 3: Total Stats */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-success-100 dark:bg-success-900/50 flex items-center justify-center">
|
||||
<DocsIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Total Available
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Global library
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Total Keywords</p>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{stats.total_keywords.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Total Search Volume</p>
|
||||
<p className="text-2xl font-bold text-success-600 dark:text-success-400">
|
||||
{stats.total_volume.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Monthly searches
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Industries Covered</p>
|
||||
<p className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{stats.industries.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import { GridIcon, PlusIcon } from "../../icons";
|
||||
import {
|
||||
fetchSites,
|
||||
Site,
|
||||
fetchKeywordStats,
|
||||
KeywordStats,
|
||||
} from "../../services/api";
|
||||
import { getDashboardStats } from "../../services/billing.api";
|
||||
import { useSiteStore } from "../../store/siteStore";
|
||||
@@ -33,6 +35,7 @@ import AutomationStatusWidget, { AutomationData } from "../../components/dashboa
|
||||
import SitesOverviewWidget from "../../components/dashboard/SitesOverviewWidget";
|
||||
import CreditsUsageWidget from "../../components/dashboard/CreditsUsageWidget";
|
||||
import AccountInfoWidget from "../../components/dashboard/AccountInfoWidget";
|
||||
import KeywordLibraryStatsWidget from "../../components/dashboard/KeywordLibraryStatsWidget";
|
||||
import { getSubscriptions, Subscription } from "../../services/billing.api";
|
||||
|
||||
export default function Home() {
|
||||
@@ -52,6 +55,8 @@ export default function Home() {
|
||||
const [showAddSite, setShowAddSite] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||
const [keywordStats, setKeywordStats] = useState<KeywordStats | null>(null);
|
||||
const [keywordStatsLoading, setKeywordStatsLoading] = useState(true);
|
||||
|
||||
// Dashboard data state
|
||||
const [attentionItems, setAttentionItems] = useState<AttentionItem[]>([]);
|
||||
@@ -114,9 +119,23 @@ export default function Home() {
|
||||
loadSites();
|
||||
loadBalance();
|
||||
loadSubscription();
|
||||
loadKeywordStats();
|
||||
loadFromBackend().catch(() => {});
|
||||
}, [loadFromBackend, loadBalance]);
|
||||
|
||||
// Load keyword stats
|
||||
const loadKeywordStats = async () => {
|
||||
try {
|
||||
setKeywordStatsLoading(true);
|
||||
const stats = await fetchKeywordStats();
|
||||
setKeywordStats(stats);
|
||||
} catch (error) {
|
||||
console.error('Failed to load keyword stats:', error);
|
||||
} finally {
|
||||
setKeywordStatsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load subscription info
|
||||
const loadSubscription = async () => {
|
||||
try {
|
||||
@@ -414,6 +433,24 @@ export default function Home() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 6: Keyword Library Stats (3 columns) */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Quick-Start Keywords — Complimentary
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Ready-to-use, pre-vetted keywords to jumpstart your content creation — no research needed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<KeywordLibraryStatsWidget
|
||||
stats={keywordStats}
|
||||
loading={keywordStatsLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add Site Button - Floating */}
|
||||
{canAddMoreSites && (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
|
||||
@@ -954,7 +954,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
<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">
|
||||
High Opportunity Keywords Complete
|
||||
Quick-Start Keywords Added Successfully
|
||||
</h3>
|
||||
<p className="text-xs text-success-700 dark:text-success-300">
|
||||
{addedCount} keywords added to your workflow
|
||||
@@ -982,11 +982,11 @@ export default function IndustriesSectorsKeywords() {
|
||||
<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">
|
||||
High Opportunity Keywords
|
||||
Quick-Start Keywords — Complimentary
|
||||
</h2>
|
||||
<Badge tone="brand" variant="soft" size="sm">
|
||||
<BoltIcon className="w-3 h-3 mr-1" />
|
||||
Curated
|
||||
Pre-Vetted
|
||||
</Badge>
|
||||
</div>
|
||||
{!allAdded && (
|
||||
@@ -1000,7 +1000,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Add top keywords for each of your sectors. Keywords will be added to your planner workflow.
|
||||
Ready-to-use keywords to jumpstart your content — no research needed. Simply add to your workflow and start creating.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1151,8 +1151,8 @@ export default function IndustriesSectorsKeywords() {
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-2xl mx-auto">
|
||||
💡 <strong>Recommended:</strong> Start by adding High Opportunity Keywords from the section above.
|
||||
They're curated for your sectors and ready to use. Once you've added those, you can browse our full keyword library below for additional targeted keywords.
|
||||
💡 <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"
|
||||
|
||||
@@ -2105,6 +2105,34 @@ export async function fetchSeedKeywords(filters?: {
|
||||
return fetchAPI(`/v1/auth/seed-keywords/${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Seed Keyword Statistics
|
||||
*/
|
||||
export interface KeywordStatsIndustry {
|
||||
name: string;
|
||||
slug: string;
|
||||
keyword_count: number;
|
||||
total_volume: number;
|
||||
}
|
||||
|
||||
export interface KeywordStatsCountry {
|
||||
country: string;
|
||||
country_display: string;
|
||||
keyword_count: number;
|
||||
total_volume: number;
|
||||
}
|
||||
|
||||
export interface KeywordStats {
|
||||
industries: KeywordStatsIndustry[];
|
||||
countries: KeywordStatsCountry[];
|
||||
total_keywords: number;
|
||||
total_volume: number;
|
||||
}
|
||||
|
||||
export async function fetchKeywordStats(): Promise<KeywordStats> {
|
||||
return fetchAPI('/v1/auth/seed-keywords/stats/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add SeedKeywords to workflow (create Keywords records)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user