ind page
This commit is contained in:
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user