620 lines
31 KiB
TypeScript
620 lines
31 KiB
TypeScript
'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<string, string> = {
|
|
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<string, React.ReactNode> = {
|
|
published: (
|
|
<svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
),
|
|
pending: (
|
|
<svg className="w-3.5 h-3.5 text-yellow-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" d="M12 6v6l4 2" />
|
|
</svg>
|
|
),
|
|
draft: (
|
|
<svg className="w-3.5 h-3.5 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
),
|
|
imported: (
|
|
<svg className="w-3.5 h-3.5 text-blue-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
),
|
|
archived: (
|
|
<svg className="w-3.5 h-3.5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
|
</svg>
|
|
),
|
|
};
|
|
|
|
// ─── 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 = (
|
|
<div className={`bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors ${href ? 'cursor-pointer' : ''}`}>
|
|
<div className="flex items-start justify-between">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${accent}`}>
|
|
{icon}
|
|
</div>
|
|
{badge && (
|
|
<span className={`text-xs font-body font-bold px-2 py-0.5 rounded-full ${badge.color}`}>
|
|
{badge.text}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-display font-bold text-white">{value}</p>
|
|
<p className="text-sm text-[#888] font-body mt-0.5">{label}</p>
|
|
{sub && <p className="text-xs text-[#555] font-body mt-1">{sub}</p>}
|
|
</div>
|
|
</div>
|
|
);
|
|
if (href) return <Link href={href}>{content}</Link>;
|
|
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 (
|
|
<Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#252525] rounded-lg hover:border-[#3b82f6] hover:bg-[#0d1a2d] transition-all group">
|
|
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${accent} group-hover:scale-105 transition-transform`}>
|
|
{icon}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-body font-semibold text-white group-hover:text-[#3b82f6] transition-colors">{label}</p>
|
|
<p className="text-xs text-[#555] font-body truncate">{description}</p>
|
|
</div>
|
|
<svg className="w-4 h-4 text-[#444] group-hover:text-[#3b82f6] ml-auto flex-shrink-0 transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
// ─── Main Page ────────────────────────────────────────────────────────────────
|
|
|
|
export default function AdminDashboardPage() {
|
|
const { user, loading } = useAuth();
|
|
const router = useRouter();
|
|
|
|
const [metrics, setMetrics] = useState<DashboardMetrics>({
|
|
totalArticles: 0,
|
|
pendingApprovals: 0,
|
|
activeContributors: 0,
|
|
publishedArticles: 0,
|
|
draftArticles: 0,
|
|
archivedArticles: 0,
|
|
totalUsers: 0,
|
|
importedArticles: 0,
|
|
});
|
|
const [recentActivity, setRecentActivity] = useState<ActivityItem[]>([]);
|
|
const [loadingData, setLoadingData] = useState(true);
|
|
const [error, setError] = useState<string | null>(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 (
|
|
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
|
<p className="text-[#555] text-sm font-body">Loading dashboard…</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#0a0a0a]">
|
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
|
|
{/* Header */}
|
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-display font-bold text-white">Admin Dashboard</h1>
|
|
<p className="text-[#555] text-sm font-body mt-1">Overview of your broadcast platform</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs text-[#555] font-body">
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" d="M12 6v6l4 2" />
|
|
</svg>
|
|
Last updated: {new Date().toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Banner */}
|
|
{error && (
|
|
<div className="mb-6 bg-red-500/10 border border-red-500/30 rounded-lg px-4 py-3 flex items-center gap-3">
|
|
<svg className="w-4 h-4 text-red-400 flex-shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01" />
|
|
</svg>
|
|
<p className="text-red-400 text-sm font-body">{error}</p>
|
|
<button onClick={fetchDashboardData} className="ml-auto text-xs text-red-400 hover:text-red-300 underline font-body">Retry</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pending Approvals Alert */}
|
|
{metrics.pendingApprovals > 0 && (
|
|
<div className="mb-6 bg-yellow-500/10 border border-yellow-500/30 rounded-lg px-4 py-3 flex items-center gap-3">
|
|
<svg className="w-4 h-4 text-yellow-400 flex-shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<p className="text-yellow-400 text-sm font-body">
|
|
<span className="font-bold">{metrics.pendingApprovals} article{metrics.pendingApprovals !== 1 ? 's' : ''}</span> awaiting approval
|
|
</p>
|
|
<Link href="/admin/articles" className="ml-auto text-xs text-yellow-400 hover:text-yellow-300 underline font-body">Review now →</Link>
|
|
</div>
|
|
)}
|
|
|
|
{/* Key Metric Cards */}
|
|
<section className="mb-8">
|
|
<h2 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-4">Key Metrics</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-4">
|
|
<MetricCard
|
|
label="Total Articles"
|
|
value={metrics.totalArticles}
|
|
sub={`${metrics.importedArticles} imported · ${metrics.totalArticles - metrics.importedArticles} native`}
|
|
accent="bg-blue-500/10"
|
|
href="/admin/articles"
|
|
icon={
|
|
<svg className="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<MetricCard
|
|
label="Pending Approvals"
|
|
value={metrics.pendingApprovals}
|
|
sub="Awaiting admin review"
|
|
accent="bg-yellow-500/10"
|
|
href="/admin/articles"
|
|
badge={metrics.pendingApprovals > 0 ? { text: 'Action needed', color: 'text-yellow-400 bg-yellow-400/10' } : undefined}
|
|
icon={
|
|
<svg className="w-5 h-5 text-yellow-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<MetricCard
|
|
label="Active Contributors"
|
|
value={metrics.activeContributors}
|
|
sub="Authors with published articles"
|
|
accent="bg-purple-500/10"
|
|
href="/admin/users"
|
|
icon={
|
|
<svg className="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<MetricCard
|
|
label="Published"
|
|
value={metrics.publishedArticles}
|
|
accent="bg-green-500/10"
|
|
href="/admin/articles"
|
|
icon={
|
|
<svg className="w-5 h-5 text-green-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<MetricCard
|
|
label="Drafts"
|
|
value={metrics.draftArticles}
|
|
accent="bg-[#1a1a1a]"
|
|
href="/admin/articles"
|
|
icon={
|
|
<svg className="w-5 h-5 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<MetricCard
|
|
label="Archived"
|
|
value={metrics.archivedArticles}
|
|
accent="bg-[#1a1a1a]"
|
|
href="/admin/articles"
|
|
icon={
|
|
<svg className="w-5 h-5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
|
</svg>
|
|
}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Main Content: Activity Feed + Quick Actions */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
{/* Recent Activity Feed */}
|
|
<div className="lg:col-span-2">
|
|
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
|
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
|
|
<h2 className="text-sm font-display font-bold text-white">Recent Activity</h2>
|
|
<Link href="/admin/articles" className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">View all →</Link>
|
|
</div>
|
|
<div className="divide-y divide-[#1a1a1a]">
|
|
{recentActivity.length === 0 ? (
|
|
<div className="px-5 py-10 text-center">
|
|
<p className="text-[#555] text-sm font-body">No recent activity found</p>
|
|
</div>
|
|
) : (
|
|
recentActivity.map((item) => (
|
|
<div key={`${item.source}-${item.id}`} className="px-5 py-3 flex items-start gap-3 hover:bg-[#0d0d0d] transition-colors">
|
|
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 mt-0.5">
|
|
{ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-white font-body truncate">{item.title}</p>
|
|
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
|
<span className="text-xs text-[#555] font-body">{item.author}</span>
|
|
<span className="text-[#333]">·</span>
|
|
<span className={`text-xs font-body font-semibold px-1.5 py-0.5 rounded ${STATUS_COLORS[item.status] || STATUS_COLORS['draft']}`}>
|
|
{item.status}
|
|
</span>
|
|
<span className="text-[#333]">·</span>
|
|
<span className="text-xs text-[#444] font-body capitalize">{item.source}</span>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs text-[#444] font-body flex-shrink-0 mt-0.5">
|
|
{item.date ? timeAgo(item.date) : '—'}
|
|
</span>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="flex flex-col gap-4">
|
|
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
|
<div className="px-5 py-4 border-b border-[#1a1a1a]">
|
|
<h2 className="text-sm font-display font-bold text-white">Quick Actions</h2>
|
|
</div>
|
|
<div className="p-4 flex flex-col gap-2">
|
|
<QuickAction
|
|
label="Review Pending Articles"
|
|
description="Approve or reject submissions"
|
|
href="/admin/articles"
|
|
accent="bg-yellow-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-yellow-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Manage Articles"
|
|
description="Edit, publish, or archive content"
|
|
href="/admin/articles"
|
|
accent="bg-blue-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Invite Contributor"
|
|
description="Send email invitation to new authors"
|
|
href="/admin/users"
|
|
accent="bg-purple-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Manage Users & Roles"
|
|
description="Assign roles and permissions"
|
|
href="/admin/users"
|
|
accent="bg-green-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-green-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Import from WordPress"
|
|
description="Bulk import WP articles"
|
|
href="/admin/import"
|
|
accent="bg-orange-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="View Analytics"
|
|
description="Performance metrics and trends"
|
|
href="/admin/analytics"
|
|
accent="bg-cyan-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-cyan-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Email Analytics"
|
|
description="Send volume, open rates, CTR & subscriber growth"
|
|
href="/admin/email-analytics"
|
|
accent="bg-blue-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Forum Moderation"
|
|
description="Pin, lock, flag threads & hide replies"
|
|
href="/admin/forum-moderation"
|
|
accent="bg-red-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-red-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Forum Dashboard"
|
|
description="Thread growth, engagement & moderation stats"
|
|
href="/admin/forum-dashboard"
|
|
accent="bg-indigo-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Roles & Permissions"
|
|
description="Assign moderator/contributor roles and manage permissions"
|
|
href="/admin/roles"
|
|
accent="bg-amber-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-amber-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Suspensions & Bans"
|
|
description="Suspend or ban users with reason and duration"
|
|
href="/admin/suspensions"
|
|
accent="bg-red-900/30"
|
|
icon={
|
|
<svg className="w-4 h-4 text-red-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" strokeLinejoin="round" d="M4.93 4.93l14.14 14.14" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Company Coverage"
|
|
description="AI story suggestions from article mentions"
|
|
href="/admin/company-coverage"
|
|
accent="bg-teal-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-teal-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 12h6m-6-4h.01" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Company Story Engine"
|
|
description="Tracked companies, story queue, pen names"
|
|
href="/admin/company-tracker"
|
|
accent="bg-emerald-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Banned Terms"
|
|
description="Manage restricted keywords and companies"
|
|
href="/admin/banned-terms"
|
|
accent="bg-red-900/20"
|
|
icon={
|
|
<svg className="w-4 h-4 text-red-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<QuickAction
|
|
label="Forum Maintenance Tool"
|
|
description="Database cleanup, repair, and maintenance utilities"
|
|
href="/admin/forum-maintenance"
|
|
accent="bg-slate-500/10"
|
|
icon={
|
|
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Summary Mini-Card */}
|
|
<div className="bg-[#111] border border-[#252525] rounded-lg p-4">
|
|
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Content Status</h3>
|
|
<div className="space-y-2">
|
|
{[
|
|
{ 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 }) => (
|
|
<div key={label}>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-xs text-[#888] font-body">{label}</span>
|
|
<span className="text-xs text-white font-body font-semibold">{value}</span>
|
|
</div>
|
|
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${color} transition-all duration-500`}
|
|
style={{ width: total > 0 ? `${Math.round((value / total) * 100)}%` : '0%' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|