Add Linker and Optimizer modules with API integration and frontend components
- Added Linker and Optimizer apps to `INSTALLED_APPS` in `settings.py`. - Configured API endpoints for Linker and Optimizer in `urls.py`. - Implemented `OptimizeContentFunction` for content optimization in the AI module. - Created prompts for content optimization and site structure generation. - Updated `OptimizerService` to utilize the new AI function for content optimization. - Developed frontend components including dashboards and content lists for Linker and Optimizer. - Integrated new routes and sidebar navigation for Linker and Optimizer in the frontend. - Enhanced content management with source and sync status filters in the Writer module. - Comprehensive test coverage added for new features and components.
This commit is contained in:
163
frontend/src/pages/Linker/Dashboard.tsx
Normal file
163
frontend/src/pages/Linker/Dashboard.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
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 { Link2, FileText, TrendingUp, ArrowRight } from 'lucide-react';
|
||||
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="Linker Dashboard" description="Internal linking overview and statistics" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Linker Dashboard"
|
||||
description="Manage internal linking for your content"
|
||||
actions={
|
||||
<Link
|
||||
to="/linker/content"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Link2 className="w-4 h-4" />
|
||||
View Content
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-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={<FileText className="w-6 h-6" />}
|
||||
trend={null}
|
||||
onClick={() => navigate('/linker/content')}
|
||||
/>
|
||||
|
||||
<EnhancedMetricCard
|
||||
title="Total Links"
|
||||
value={stats.totalLinks.toString()}
|
||||
subtitle="Internal links created"
|
||||
icon={<Link2 className="w-6 h-6" />}
|
||||
trend={null}
|
||||
onClick={() => navigate('/linker/content')}
|
||||
/>
|
||||
|
||||
<EnhancedMetricCard
|
||||
title="Avg Links/Content"
|
||||
value={stats.averageLinksPerContent.toString()}
|
||||
subtitle="Average per linked content"
|
||||
icon={<TrendingUp className="w-6 h-6" />}
|
||||
trend={null}
|
||||
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">
|
||||
<Link2 className="w-5 h-5 text-blue-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>
|
||||
<ArrowRight 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">
|
||||
<FileText 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>
|
||||
<ArrowRight 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user