193 lines
7.9 KiB
TypeScript
193 lines
7.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import PageMeta from '../../components/common/PageMeta';
|
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
|
import { fetchIndustries, Industry, fetchSeedKeywords, SeedKeyword } from '../../services/api';
|
|
import { Card } from '../../components/ui/card';
|
|
import Badge from '../../components/ui/badge/Badge';
|
|
import PageHeader from '../../components/common/PageHeader';
|
|
import { PieChartIcon } from '../../icons';
|
|
import { Tooltip } from '../../components/ui/tooltip/Tooltip';
|
|
|
|
interface IndustryWithData extends Industry {
|
|
keywordsCount: number;
|
|
topKeywords: SeedKeyword[];
|
|
totalVolume: number;
|
|
}
|
|
|
|
// Format volume with k for thousands and m for millions
|
|
const formatVolume = (volume: number): string => {
|
|
if (volume >= 1000000) {
|
|
return `${(volume / 1000000).toFixed(1)}m`;
|
|
} else if (volume >= 1000) {
|
|
return `${(volume / 1000).toFixed(1)}k`;
|
|
}
|
|
return volume.toString();
|
|
};
|
|
|
|
export default function Industries() {
|
|
const toast = useToast();
|
|
const [industries, setIndustries] = useState<IndustryWithData[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadIndustries();
|
|
}, []);
|
|
|
|
const loadIndustries = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetchIndustries();
|
|
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);
|
|
|
|
// Calculate total volume of all keywords
|
|
const totalVolume = industryKeywords.reduce(
|
|
(sum, kw) => sum + (kw.volume || 0),
|
|
0
|
|
);
|
|
|
|
return {
|
|
...industry,
|
|
keywordsCount: industryKeywords.length,
|
|
topKeywords,
|
|
totalVolume,
|
|
};
|
|
});
|
|
|
|
// Filter to only show industries that have keywords associated
|
|
const industriesWithKeywords = industriesWithData.filter(
|
|
(industry) => industry.keywordsCount > 0
|
|
);
|
|
|
|
setIndustries(industriesWithKeywords);
|
|
} catch (error: any) {
|
|
toast.error(`Failed to load industries: ${error.message}`);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<PageMeta title="Industries" />
|
|
<PageHeader
|
|
title="Industries"
|
|
badge={{ icon: <PieChartIcon />, color: 'blue' }}
|
|
hideSiteSector={true}
|
|
/>
|
|
<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 ? (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-gray-500">Loading industries...</div>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{industries.map((industry) => (
|
|
<Card
|
|
key={industry.slug}
|
|
className="p-4 hover:shadow-lg transition-shadow duration-200 border border-gray-200 dark:border-gray-700"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex justify-between items-start mb-3">
|
|
<h3 className="text-base font-bold text-gray-900 dark:text-white leading-tight">
|
|
{industry.name}
|
|
</h3>
|
|
{industry.totalVolume > 0 && (
|
|
<Tooltip
|
|
text={`Total search volume: ${industry.totalVolume.toLocaleString()} monthly searches across all keywords in this industry`}
|
|
placement="top"
|
|
>
|
|
<Badge variant="solid" color="dark" size="sm">
|
|
{formatVolume(industry.totalVolume)}
|
|
</Badge>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
|
|
{/* Description - Compact */}
|
|
{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>
|
|
</>
|
|
);
|
|
}
|
|
|