'use client'; import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; // ─── Types ──────────────────────────────────────────────────────────────────── interface DashboardMetrics { totalArticles: number; pendingApprovals: number; activeContributors: number; publishedArticles: number; draftArticles: number; archivedArticles: number; totalUsers: number; importedArticles: number; } interface ActivityItem { id: string; type: 'published' | 'pending' | 'imported' | 'draft' | 'archived'; title: string; author: string; source: 'imported' | 'native'; date: string; status: string; } // ─── Helpers ────────────────────────────────────────────────────────────────── function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); return `${days}d ago`; } const STATUS_COLORS: Record = { published: 'text-green-400 bg-green-400/10', pending: 'text-yellow-400 bg-yellow-400/10', draft: 'text-[#888] bg-[#1a1a1a]', archived: 'text-[#555] bg-[#1a1a1a]', imported: 'text-blue-400 bg-blue-400/10', }; const ACTIVITY_ICONS: Record = { published: ( ), pending: ( ), draft: ( ), imported: ( ), archived: ( ), }; // ─── Metric Card ────────────────────────────────────────────────────────────── interface MetricCardProps { label: string; value: number | string; sub?: string; accent: string; icon: React.ReactNode; href?: string; badge?: { text: string; color: string }; } function MetricCard({ label, value, sub, accent, icon, href, badge }: MetricCardProps) { const content = (
{icon}
{badge && ( {badge.text} )}

{value}

{label}

{sub &&

{sub}

}
); if (href) return {content}; return content; } // ─── Quick Action Widget ─────────────────────────────────────────────────────── interface QuickActionProps { label: string; description: string; href: string; icon: React.ReactNode; accent: string; } function QuickAction({ label, description, href, icon, accent }: QuickActionProps) { return (
{icon}

{label}

{description}

); } // ─── Main Page ──────────────────────────────────────────────────────────────── export default function AdminDashboardPage() { const { user, loading } = useAuth(); const router = useRouter(); const [metrics, setMetrics] = useState({ totalArticles: 0, pendingApprovals: 0, activeContributors: 0, publishedArticles: 0, draftArticles: 0, archivedArticles: 0, totalUsers: 0, importedArticles: 0, }); const [recentActivity, setRecentActivity] = useState([]); const [loadingData, setLoadingData] = useState(true); const [error, setError] = useState(null); const fetchDashboardData = useCallback(async () => { try { setLoadingData(true); setError(null); // Fetch articles data const articlesRes = await fetch('/api/admin/articles'); if (!articlesRes.ok) throw new Error('Failed to fetch articles'); const articlesData = await articlesRes.json(); const allArticles: any[] = [ ...(articlesData.imported || []), ...(articlesData.native || []), ]; // Compute metrics const totalArticles = allArticles.length; const pendingApprovals = allArticles.filter((a) => a.status === 'pending').length; const publishedArticles = allArticles.filter((a) => a.status === 'published').length; const draftArticles = allArticles.filter((a) => a.status === 'draft').length; const archivedArticles = allArticles.filter((a) => a.status === 'archived').length; const importedArticles = (articlesData.imported || []).length; // Active contributors: unique author_names with at least one published article const contributorSet = new Set( allArticles .filter((a) => a.status === 'published' && a.author_name) .map((a) => a.author_name) ); const activeContributors = contributorSet.size; setMetrics({ totalArticles, pendingApprovals, activeContributors, publishedArticles, draftArticles, archivedArticles, totalUsers: 0, importedArticles, }); // Build recent activity feed (latest 12 articles sorted by date) const sorted = [...allArticles] .sort((a, b) => new Date(b.date || b.created_at || 0).getTime() - new Date(a.date || a.created_at || 0).getTime()) .slice(0, 12); setRecentActivity( sorted.map((a) => ({ id: String(a.id), type: a.status as ActivityItem['type'], title: a.title || 'Untitled', author: a.author_name || 'Unknown', source: a.source, date: a.date || a.created_at || '', status: a.status, })) ); } catch (err: any) { setError(err.message || 'Failed to load dashboard data'); } finally { setLoadingData(false); } }, []); useEffect(() => { if (!loading && !user) { router.push('/login'); } }, [user, loading, router]); useEffect(() => { if (user) fetchDashboardData(); }, [user, fetchDashboardData]); if (loading || loadingData) { return (

Loading dashboard…

); } if (!user) return null; return (
{/* Header */}

Admin Dashboard

Overview of your broadcast platform

Last updated: {new Date().toLocaleTimeString()}
{/* Error Banner */} {error && (

{error}

)} {/* Pending Approvals Alert */} {metrics.pendingApprovals > 0 && (

{metrics.pendingApprovals} article{metrics.pendingApprovals !== 1 ? 's' : ''} awaiting approval

Review now →
)} {/* Key Metric Cards */}

Key Metrics

} /> 0 ? { text: 'Action needed', color: 'text-yellow-400 bg-yellow-400/10' } : undefined} icon={ } /> } /> } /> } /> } />
{/* Main Content: Activity Feed + Quick Actions */}
{/* Recent Activity Feed */}

Recent Activity

View all →
{recentActivity.length === 0 ? (

No recent activity found

) : ( recentActivity.map((item) => (
{ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']}

{item.title}

{item.author} · {item.status} · {item.source}
{item.date ? timeAgo(item.date) : '—'}
)) )}
{/* Quick Actions */}

Quick Actions

} /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
{/* Status Summary Mini-Card */}

Content Status

{[ { label: 'Published', value: metrics.publishedArticles, color: 'bg-green-400', total: metrics.totalArticles }, { label: 'Pending', value: metrics.pendingApprovals, color: 'bg-yellow-400', total: metrics.totalArticles }, { label: 'Draft', value: metrics.draftArticles, color: 'bg-[#444]', total: metrics.totalArticles }, { label: 'Archived', value: metrics.archivedArticles, color: 'bg-[#2a2a2a]', total: metrics.totalArticles }, ].map(({ label, value, color, total }) => (
{label} {value}
0 ? `${Math.round((value / total) * 100)}%` : '0%' }} />
))}
); }