more impeorventes for kewyrods libreary
This commit is contained in:
@@ -873,6 +873,66 @@ class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='stats', url_name='stats')
|
||||||
|
def stats(self, request):
|
||||||
|
"""
|
||||||
|
Get aggregated keyword statistics by industry and country.
|
||||||
|
Returns top industries and countries with keyword counts and total volume.
|
||||||
|
"""
|
||||||
|
from django.db.models import Count, Sum, Q
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Top industries by keyword count
|
||||||
|
industries = Industry.objects.annotate(
|
||||||
|
keyword_count=Count('seed_keywords', filter=Q(seed_keywords__is_active=True)),
|
||||||
|
total_volume=Sum('seed_keywords__volume', filter=Q(seed_keywords__is_active=True))
|
||||||
|
).filter(
|
||||||
|
keyword_count__gt=0
|
||||||
|
).order_by('-keyword_count')[:10]
|
||||||
|
|
||||||
|
industries_data = [{
|
||||||
|
'name': ind.name,
|
||||||
|
'slug': ind.slug,
|
||||||
|
'keyword_count': ind.keyword_count or 0,
|
||||||
|
'total_volume': ind.total_volume or 0,
|
||||||
|
} for ind in industries]
|
||||||
|
|
||||||
|
# Keywords by country
|
||||||
|
countries = SeedKeyword.objects.filter(
|
||||||
|
is_active=True
|
||||||
|
).values('country').annotate(
|
||||||
|
keyword_count=Count('id'),
|
||||||
|
total_volume=Sum('volume')
|
||||||
|
).order_by('-keyword_count')
|
||||||
|
|
||||||
|
countries_data = [{
|
||||||
|
'country': c['country'],
|
||||||
|
'country_display': dict(SeedKeyword.COUNTRY_CHOICES).get(c['country'], c['country']),
|
||||||
|
'keyword_count': c['keyword_count'],
|
||||||
|
'total_volume': c['total_volume'] or 0,
|
||||||
|
} for c in countries]
|
||||||
|
|
||||||
|
# Total stats
|
||||||
|
total_stats = SeedKeyword.objects.filter(is_active=True).aggregate(
|
||||||
|
total_keywords=Count('id'),
|
||||||
|
total_volume=Sum('volume')
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'industries': industries_data,
|
||||||
|
'countries': countries_data,
|
||||||
|
'total_keywords': total_stats['total_keywords'] or 0,
|
||||||
|
'total_volume': total_stats['total_volume'] or 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return success_response(data=data, request=request)
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(
|
||||||
|
error=f'Failed to fetch keyword stats: {str(e)}',
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
@action(detail=False, methods=['post'], url_path='import_seed_keywords', url_name='import_seed_keywords')
|
@action(detail=False, methods=['post'], url_path='import_seed_keywords', url_name='import_seed_keywords')
|
||||||
def import_seed_keywords(self, request):
|
def import_seed_keywords(self, request):
|
||||||
"""
|
"""
|
||||||
|
|||||||
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 {
|
import {
|
||||||
fetchSites,
|
fetchSites,
|
||||||
Site,
|
Site,
|
||||||
|
fetchKeywordStats,
|
||||||
|
KeywordStats,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import { getDashboardStats } from "../../services/billing.api";
|
import { getDashboardStats } from "../../services/billing.api";
|
||||||
import { useSiteStore } from "../../store/siteStore";
|
import { useSiteStore } from "../../store/siteStore";
|
||||||
@@ -33,6 +35,7 @@ import AutomationStatusWidget, { AutomationData } from "../../components/dashboa
|
|||||||
import SitesOverviewWidget from "../../components/dashboard/SitesOverviewWidget";
|
import SitesOverviewWidget from "../../components/dashboard/SitesOverviewWidget";
|
||||||
import CreditsUsageWidget from "../../components/dashboard/CreditsUsageWidget";
|
import CreditsUsageWidget from "../../components/dashboard/CreditsUsageWidget";
|
||||||
import AccountInfoWidget from "../../components/dashboard/AccountInfoWidget";
|
import AccountInfoWidget from "../../components/dashboard/AccountInfoWidget";
|
||||||
|
import KeywordLibraryStatsWidget from "../../components/dashboard/KeywordLibraryStatsWidget";
|
||||||
import { getSubscriptions, Subscription } from "../../services/billing.api";
|
import { getSubscriptions, Subscription } from "../../services/billing.api";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -52,6 +55,8 @@ export default function Home() {
|
|||||||
const [showAddSite, setShowAddSite] = useState(false);
|
const [showAddSite, setShowAddSite] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||||
|
const [keywordStats, setKeywordStats] = useState<KeywordStats | null>(null);
|
||||||
|
const [keywordStatsLoading, setKeywordStatsLoading] = useState(true);
|
||||||
|
|
||||||
// Dashboard data state
|
// Dashboard data state
|
||||||
const [attentionItems, setAttentionItems] = useState<AttentionItem[]>([]);
|
const [attentionItems, setAttentionItems] = useState<AttentionItem[]>([]);
|
||||||
@@ -114,9 +119,23 @@ export default function Home() {
|
|||||||
loadSites();
|
loadSites();
|
||||||
loadBalance();
|
loadBalance();
|
||||||
loadSubscription();
|
loadSubscription();
|
||||||
|
loadKeywordStats();
|
||||||
loadFromBackend().catch(() => {});
|
loadFromBackend().catch(() => {});
|
||||||
}, [loadFromBackend, loadBalance]);
|
}, [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
|
// Load subscription info
|
||||||
const loadSubscription = async () => {
|
const loadSubscription = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -414,6 +433,24 @@ export default function Home() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Add Site Button - Floating */}
|
||||||
{canAddMoreSites && (
|
{canAddMoreSites && (
|
||||||
<div className="fixed bottom-6 right-6 z-50">
|
<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" />
|
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-success-900 dark:text-success-200">
|
<h3 className="text-sm font-medium text-success-900 dark:text-success-200">
|
||||||
High Opportunity Keywords Complete
|
Quick-Start Keywords Added Successfully
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-success-700 dark:text-success-300">
|
<p className="text-xs text-success-700 dark:text-success-300">
|
||||||
{addedCount} keywords added to your workflow
|
{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 justify-between mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
High Opportunity Keywords
|
Quick-Start Keywords — Complimentary
|
||||||
</h2>
|
</h2>
|
||||||
<Badge tone="brand" variant="soft" size="sm">
|
<Badge tone="brand" variant="soft" size="sm">
|
||||||
<BoltIcon className="w-3 h-3 mr-1" />
|
<BoltIcon className="w-3 h-3 mr-1" />
|
||||||
Curated
|
Pre-Vetted
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{!allAdded && (
|
{!allAdded && (
|
||||||
@@ -1000,7 +1000,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1151,8 +1151,8 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-2xl mx-auto">
|
<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.
|
💡 <strong>Recommended:</strong> Start with the complimentary Quick-Start Keywords above to accelerate your workflow.
|
||||||
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.
|
They're pre-vetted and ready to use immediately. Once added, you can browse our full library below for additional targeted keywords.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -2105,6 +2105,34 @@ export async function fetchSeedKeywords(filters?: {
|
|||||||
return fetchAPI(`/v1/auth/seed-keywords/${queryString ? `?${queryString}` : ''}`);
|
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)
|
* Add SeedKeywords to workflow (create Keywords records)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user