This commit is contained in:
Desktop
2025-11-13 00:59:55 +05:00
parent 235f01c1fe
commit 31bfadf38a
2 changed files with 135 additions and 33 deletions

View File

@@ -1,13 +1,20 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta'; import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer'; import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchIndustries, Industry } from '../../services/api'; import { fetchIndustries, Industry, fetchSeedKeywords, SeedKeyword } from '../../services/api';
import { Card } from '../../components/ui/card'; import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge'; import Badge from '../../components/ui/badge/Badge';
import PageHeader from '../../components/common/PageHeader';
import { PieChartIcon } from '../../icons';
interface IndustryWithData extends Industry {
keywordsCount: number;
topKeywords: SeedKeyword[];
}
export default function Industries() { export default function Industries() {
const toast = useToast(); const toast = useToast();
const [industries, setIndustries] = useState<Industry[]>([]); const [industries, setIndustries] = useState<IndustryWithData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -18,7 +25,40 @@ export default function Industries() {
try { try {
setLoading(true); setLoading(true);
const response = await fetchIndustries(); const response = await fetchIndustries();
setIndustries(response.industries || []); const industriesList = response.industries || [];
// First, fetch all seed keywords once (more efficient than per-industry)
// We'll fetch a larger sample to get accurate counts and top keywords
let allKeywords: SeedKeyword[] = [];
try {
const keywordsResponse = await fetchSeedKeywords({
page_size: 1000, // Get a large sample for accurate counts
});
allKeywords = keywordsResponse.results || [];
} catch (error) {
console.warn('Failed to fetch keywords, will show without keyword data:', error);
}
// Process each industry with its keywords data
const industriesWithData = industriesList.map((industry) => {
// Filter keywords by industry name (matching industry.name)
const industryKeywords = allKeywords.filter(
(kw: SeedKeyword) => kw.industry_name === industry.name
);
// Sort by volume and get top 5
const topKeywords = [...industryKeywords]
.sort((a, b) => (b.volume || 0) - (a.volume || 0))
.slice(0, 5);
return {
...industry,
keywordsCount: industryKeywords.length,
topKeywords,
};
});
setIndustries(industriesWithData);
} catch (error: any) { } catch (error: any) {
toast.error(`Failed to load industries: ${error.message}`); toast.error(`Failed to load industries: ${error.message}`);
} finally { } finally {
@@ -27,38 +67,96 @@ export default function Industries() {
}; };
return ( return (
<div className="p-6"> <>
<PageMeta title="Industries" /> <PageMeta title="Industries" />
<div className="mb-6"> <PageHeader
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Industries</h1> title="Industries"
<p className="text-gray-600 dark:text-gray-400 mt-1">Global industry reference data</p> badge={{ icon: <PieChartIcon />, color: 'blue' }}
</div> />
<div className="p-6">
<div className="mb-6">
<p className="text-gray-600 dark:text-gray-400">
Explore our comprehensive global database of industries, sectors, and high-volume keywords
</p>
</div>
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div> <div className="text-gray-500">Loading industries...</div>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{industries.map((industry) => ( {industries.map((industry) => (
<Card key={industry.id} className="p-6"> <Card
<div className="flex justify-between items-start mb-4"> key={industry.slug}
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{industry.name}</h3> className="p-4 hover:shadow-lg transition-shadow duration-200 border border-gray-200 dark:border-gray-700"
<Badge variant="light" color={industry.is_active ? 'success' : 'dark'}> >
{industry.is_active ? 'Active' : 'Inactive'} {/* Header */}
</Badge> <div className="flex justify-between items-start mb-3">
</div> <h3 className="text-base font-bold text-gray-900 dark:text-white leading-tight">
{industry.description && ( {industry.name}
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">{industry.description}</p> </h3>
)} {industry.is_active !== false && (
<p className="text-sm text-gray-500 dark:text-gray-400"> <Badge variant="light" color="success" size="xs">
Sectors: {industry.sectors_count || 0} Active
</p> </Badge>
</Card> )}
))} </div>
</div>
)} {/* Description - Compact */}
</div> {industry.description && (
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
{industry.description}
</p>
)}
{/* Stats Row - Compact */}
<div className="flex items-center gap-4 mb-3 text-xs">
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<span className="font-medium">{industry.sectors?.length || industry.sectors_count || 0}</span>
<span className="text-gray-500">sectors</span>
</div>
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
<span className="font-medium">{industry.keywordsCount || 0}</span>
<span className="text-gray-500">keywords</span>
</div>
</div>
{/* Top Keywords Section */}
{industry.topKeywords && industry.topKeywords.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<p className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Top Keywords
</p>
<div className="flex flex-wrap gap-1.5">
{industry.topKeywords.slice(0, 5).map((keyword, idx) => (
<div
key={keyword.id || idx}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
>
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">
{keyword.keyword}
</span>
<span className="text-xs text-blue-600 dark:text-blue-400 font-semibold">
{keyword.volume ? (keyword.volume >= 1000 ? `${(keyword.volume / 1000).toFixed(1)}k` : keyword.volume.toString()) : '-'}
</span>
</div>
))}
</div>
</div>
)}
</Card>
))}
</div>
)}
</div>
</>
); );
} }

View File

@@ -1242,10 +1242,14 @@ export async function fetchSiteSectors(siteId: number): Promise<any[]> {
// Industries API functions // Industries API functions
export interface Industry { export interface Industry {
id?: number;
name: string; name: string;
slug: string; slug: string;
description: string; description: string;
sectors: Sector[]; sectors: Sector[];
sectors_count?: number;
keywords_count?: number;
is_active?: boolean;
} }
export interface Sector { export interface Sector {