171 lines
6.4 KiB
TypeScript
171 lines
6.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import PageMeta from '../../components/common/PageMeta';
|
|
import ComponentCard from '../../components/common/ComponentCard';
|
|
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
|
|
import PageHeader from '../../components/common/PageHeader';
|
|
import { FileTextIcon, ArrowRightIcon, PlugInIcon, ArrowUpIcon } from '../../icons';
|
|
import { fetchContent } from '../../services/api';
|
|
import { useSiteStore } from '../../store/siteStore';
|
|
import { useSectorStore } from '../../store/sectorStore';
|
|
|
|
interface LinkerStats {
|
|
totalLinked: number;
|
|
totalLinks: number;
|
|
averageLinksPerContent: number;
|
|
contentWithLinks: number;
|
|
contentWithoutLinks: number;
|
|
}
|
|
|
|
export default function LinkerDashboard() {
|
|
const navigate = useNavigate();
|
|
const { activeSite } = useSiteStore();
|
|
const { activeSector } = useSectorStore();
|
|
|
|
const [stats, setStats] = useState<LinkerStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetchDashboardData();
|
|
}, [activeSite, activeSector]);
|
|
|
|
const fetchDashboardData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Fetch content to calculate stats
|
|
const contentRes = await fetchContent({
|
|
page_size: 1000,
|
|
sector_id: activeSector?.id,
|
|
});
|
|
|
|
const content = contentRes.results || [];
|
|
|
|
// Calculate stats
|
|
const contentWithLinks = content.filter(c => c.internal_links && c.internal_links.length > 0);
|
|
const totalLinks = content.reduce((sum, c) => sum + (c.internal_links?.length || 0), 0);
|
|
const averageLinksPerContent = contentWithLinks.length > 0
|
|
? (totalLinks / contentWithLinks.length)
|
|
: 0;
|
|
|
|
setStats({
|
|
totalLinked: contentWithLinks.length,
|
|
totalLinks,
|
|
averageLinksPerContent: parseFloat(averageLinksPerContent.toFixed(1)),
|
|
contentWithLinks: contentWithLinks.length,
|
|
contentWithoutLinks: content.length - contentWithLinks.length,
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Error loading linker stats:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<PageMeta title="Internal Linking Dashboard" description="Track your internal linking progress" />
|
|
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<PageHeader
|
|
title="Internal Linking Dashboard"
|
|
lastUpdated={new Date()}
|
|
badge={{
|
|
icon: <PlugInIcon />,
|
|
color: 'blue',
|
|
}}
|
|
/>
|
|
<Link
|
|
to="/linker/content"
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
|
>
|
|
<PlugInIcon />
|
|
View Content
|
|
</Link>
|
|
</div>
|
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
Manage internal linking for your content
|
|
</p>
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12">
|
|
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
|
|
<p className="mt-2 text-gray-600 dark:text-gray-400">Loading stats...</p>
|
|
</div>
|
|
) : stats ? (
|
|
<>
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<EnhancedMetricCard
|
|
title="Total Linked"
|
|
value={stats.totalLinked.toString()}
|
|
subtitle={`${stats.contentWithoutLinks} without links`}
|
|
icon={<FileTextIcon className="w-6 h-6" />}
|
|
accentColor="blue"
|
|
onClick={() => navigate('/linker/content')}
|
|
/>
|
|
|
|
<EnhancedMetricCard
|
|
title="Total Links"
|
|
value={stats.totalLinks.toString()}
|
|
subtitle="Internal links created"
|
|
icon={<PlugInIcon className="w-6 h-6" />}
|
|
accentColor="purple"
|
|
onClick={() => navigate('/linker/content')}
|
|
/>
|
|
|
|
<EnhancedMetricCard
|
|
title="Avg Links/Content"
|
|
value={stats.averageLinksPerContent.toString()}
|
|
subtitle="Average per linked content"
|
|
icon={<ArrowUpIcon className="w-6 h-6" />}
|
|
accentColor="green"
|
|
onClick={() => navigate('/linker/content')}
|
|
/>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<ComponentCard title="Quick Actions" className="mt-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Link
|
|
to="/linker/content"
|
|
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<PlugInIcon className="w-5 h-5 text-brand-500" />
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">Link Content</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Process content for internal linking</p>
|
|
</div>
|
|
</div>
|
|
<ArrowRightIcon className="w-5 h-5 text-gray-400" />
|
|
</Link>
|
|
|
|
<Link
|
|
to="/writer/content"
|
|
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<FileTextIcon className="w-5 h-5 text-purple-500" />
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">View Content</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Browse all content items</p>
|
|
</div>
|
|
</div>
|
|
<ArrowRightIcon className="w-5 h-5 text-gray-400" />
|
|
</Link>
|
|
</div>
|
|
</ComponentCard>
|
|
</>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-600 dark:text-gray-400">No data available</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|