Files
igny8/frontend/src/pages/Reference/Industries.tsx
2025-11-13 01:20:47 +05:00

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>
</>
);
}