- Login page: stone/dark palette, liquid ambient blobs, dk0.dev branding, gradient accent bar, large rounded card, site-matching button/input styles - Lockout/loading states: dark mode support, emerald spinner, red gradient bar - Dashboard navbar: gradient accent bar, monospace branding, pill-style tab buttons with dark/light active state, improved mobile menu grid layout - Stats cards: liquid-* gradient backgrounds per metric (emerald, sky, purple, amber, pink, teal) with matching icon colors and rounded-3xl corners - Section headings: uppercase tracking-tighter font-black with emerald accent dot - Activity/settings cards: white/dark-stone background, rounded-3xl, dark mode - Removed framer-motion from manage/page.tsx; replaced admin-glass* CSS classes with proper Tailwind throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
641 lines
28 KiB
TypeScript
641 lines
28 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
Mail,
|
|
Settings,
|
|
TrendingUp,
|
|
Plus,
|
|
Shield,
|
|
Users,
|
|
Activity,
|
|
Database,
|
|
Home,
|
|
LogOut,
|
|
Menu,
|
|
X
|
|
} from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import dynamic from 'next/dynamic';
|
|
|
|
const EmailManager = dynamic(
|
|
() => import('./EmailManager').then((m) => m.EmailManager),
|
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading emails…</div> }
|
|
);
|
|
const AnalyticsDashboard = dynamic(
|
|
() => import('./AnalyticsDashboard').then((m) => m.default),
|
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading analytics…</div> }
|
|
);
|
|
const ImportExport = dynamic(
|
|
() => import('./ImportExport').then((m) => m.default),
|
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading tools…</div> }
|
|
);
|
|
const ProjectManager = dynamic(
|
|
() => import('./ProjectManager').then((m) => m.ProjectManager),
|
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading projects…</div> }
|
|
);
|
|
const ContentManager = dynamic(
|
|
() => import('./ContentManager').then((m) => m.default),
|
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading content…</div> }
|
|
);
|
|
|
|
interface Project {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
content?: string;
|
|
category: string;
|
|
difficulty?: string;
|
|
tags?: string[];
|
|
featured: boolean;
|
|
published: boolean;
|
|
github?: string;
|
|
live?: string;
|
|
image?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
analytics?: {
|
|
views: number;
|
|
likes: number;
|
|
shares: number;
|
|
};
|
|
performance?: {
|
|
lighthouse: number;
|
|
};
|
|
}
|
|
|
|
interface ModernAdminDashboardProps {
|
|
isAuthenticated?: boolean;
|
|
}
|
|
|
|
type TabId = 'overview' | 'projects' | 'emails' | 'analytics' | 'content' | 'settings';
|
|
|
|
const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthenticated = true }) => {
|
|
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const [analytics, setAnalytics] = useState<Record<string, unknown> | null>(null);
|
|
const [emails, setEmails] = useState<Record<string, unknown>[]>([]);
|
|
const [systemStats, setSystemStats] = useState<Record<string, unknown> | null>(null);
|
|
|
|
const loadProjects = useCallback(async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const sessionToken = sessionStorage.getItem('admin_session_token');
|
|
const response = await fetch('/api/projects', {
|
|
headers: {
|
|
'x-admin-request': 'true',
|
|
'x-session-token': sessionToken || ''
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.warn('Failed to load projects:', response.status);
|
|
}
|
|
setProjects([]);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
setProjects(data.projects || []);
|
|
} catch {
|
|
setProjects([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const loadAnalytics = useCallback(async () => {
|
|
try {
|
|
const sessionToken = sessionStorage.getItem('admin_session_token');
|
|
const response = await fetch('/api/analytics/dashboard', {
|
|
headers: {
|
|
'x-admin-request': 'true',
|
|
'x-session-token': sessionToken || ''
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setAnalytics(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading analytics:', error);
|
|
}
|
|
}, []);
|
|
|
|
const loadEmails = useCallback(async () => {
|
|
try {
|
|
const sessionToken = sessionStorage.getItem('admin_session_token');
|
|
const response = await fetch('/api/contacts', {
|
|
headers: {
|
|
'x-admin-request': 'true',
|
|
'x-session-token': sessionToken || ''
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setEmails(data.contacts || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading emails:', error);
|
|
}
|
|
}, []);
|
|
|
|
const loadSystemStats = useCallback(async () => {
|
|
try {
|
|
const sessionToken = sessionStorage.getItem('admin_session_token');
|
|
const response = await fetch('/api/health', {
|
|
headers: {
|
|
'x-admin-request': 'true',
|
|
'x-session-token': sessionToken || ''
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setSystemStats(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading system stats:', error);
|
|
}
|
|
}, []);
|
|
|
|
const loadAllData = useCallback(async () => {
|
|
await Promise.all([
|
|
loadProjects(),
|
|
loadAnalytics(),
|
|
loadEmails(),
|
|
loadSystemStats()
|
|
]);
|
|
}, [loadProjects, loadAnalytics, loadEmails, loadSystemStats]);
|
|
|
|
// Real stats from API data
|
|
const stats = {
|
|
totalProjects: projects.length,
|
|
publishedProjects: projects.filter(p => p.published).length,
|
|
totalViews: ((analytics?.overview as Record<string, unknown>)?.totalViews as number) || (analytics?.totalViews as number) || projects.reduce((sum, p) => sum + (p.analytics?.views || 0), 0),
|
|
unreadEmails: emails.filter(e => !(e.read as boolean)).length,
|
|
avgPerformance: (() => {
|
|
const projectsWithPerf = projects.filter(p => {
|
|
const perf = p.performance as Record<string, unknown> || {};
|
|
return (perf.lighthouse as number || 0) > 0;
|
|
});
|
|
if (projectsWithPerf.length === 0) return 0;
|
|
return Math.round(projectsWithPerf.reduce((sum, p) => {
|
|
const perf = p.performance as Record<string, unknown> || {};
|
|
return sum + (perf.lighthouse as number || 0);
|
|
}, 0) / projectsWithPerf.length);
|
|
})(),
|
|
systemHealth: (systemStats?.status as string) || 'unknown',
|
|
totalUsers: ((analytics?.metrics as Record<string, unknown>)?.totalUsers as number) || (analytics?.totalUsers as number) || 0,
|
|
bounceRate: ((analytics?.metrics as Record<string, unknown>)?.bounceRate as number) || (analytics?.bounceRate as number) || 0,
|
|
avgSessionDuration: ((analytics?.metrics as Record<string, unknown>)?.avgSessionDuration as number) || (analytics?.avgSessionDuration as number) || 0
|
|
};
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
await Promise.all([loadProjects(), loadSystemStats()]);
|
|
|
|
const idle = (cb: () => void) => {
|
|
if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
|
|
(window as unknown as { requestIdleCallback: (fn: () => void) => void }).requestIdleCallback(cb);
|
|
} else {
|
|
setTimeout(cb, 300);
|
|
}
|
|
};
|
|
|
|
idle(() => {
|
|
void loadAnalytics();
|
|
void loadEmails();
|
|
});
|
|
})();
|
|
}, [loadProjects, loadSystemStats, loadAnalytics, loadEmails]);
|
|
|
|
const navigation = [
|
|
{ id: 'overview' as TabId, label: 'Dashboard', icon: Home, description: 'Overview & Statistics' },
|
|
{ id: 'projects' as TabId, label: 'Projects', icon: Database, description: 'Manage Projects' },
|
|
{ id: 'emails' as TabId, label: 'Emails', icon: Mail, description: 'Email Management' },
|
|
{ id: 'analytics' as TabId, label: 'Analytics', icon: Activity, description: 'Site Analytics' },
|
|
{ id: 'content' as TabId, label: 'Content', icon: Shield, description: 'Texts, pages & localization' },
|
|
{ id: 'settings' as TabId, label: 'Settings', icon: Settings, description: 'System Settings' }
|
|
];
|
|
|
|
const statCards = [
|
|
{
|
|
label: 'Projects',
|
|
value: stats.totalProjects,
|
|
sub: `${stats.publishedProjects} published`,
|
|
icon: Database,
|
|
tab: 'projects' as TabId,
|
|
gradient: 'from-emerald-400/20 to-emerald-400/5',
|
|
border: 'border-emerald-400/20 dark:border-emerald-400/10',
|
|
iconColor: 'text-emerald-500',
|
|
tooltip: 'REAL DATA: Total projects in your portfolio from the database. Shows published vs unpublished count.',
|
|
},
|
|
{
|
|
label: 'Page Views',
|
|
value: stats.totalViews.toLocaleString(),
|
|
sub: `${stats.totalUsers} users`,
|
|
icon: Activity,
|
|
tab: 'analytics' as TabId,
|
|
gradient: 'from-sky-400/20 to-sky-400/5',
|
|
border: 'border-sky-400/20 dark:border-sky-400/10',
|
|
iconColor: 'text-sky-500',
|
|
tooltip: 'REAL DATA: Total page views from PageView table (last 30 days). Users = unique IP addresses.',
|
|
},
|
|
{
|
|
label: 'Messages',
|
|
value: emails.length,
|
|
sub: stats.unreadEmails > 0 ? `${stats.unreadEmails} unread` : 'all read',
|
|
subColor: stats.unreadEmails > 0 ? 'text-red-500' : 'text-emerald-500',
|
|
icon: Mail,
|
|
tab: 'emails' as TabId,
|
|
gradient: 'from-purple-400/20 to-purple-400/5',
|
|
border: 'border-purple-400/20 dark:border-purple-400/10',
|
|
iconColor: 'text-purple-500',
|
|
},
|
|
{
|
|
label: 'Performance',
|
|
value: stats.avgPerformance || 'N/A',
|
|
sub: 'Lighthouse score',
|
|
icon: TrendingUp,
|
|
tab: 'analytics' as TabId,
|
|
gradient: 'from-amber-400/20 to-amber-400/5',
|
|
border: 'border-amber-400/20 dark:border-amber-400/10',
|
|
iconColor: 'text-amber-500',
|
|
tooltip: stats.avgPerformance > 0
|
|
? 'REAL DATA: Average Lighthouse score from real Web Vitals collected from page visits.'
|
|
: 'No performance data yet. Scores appear after visitors load pages.',
|
|
},
|
|
{
|
|
label: 'Bounce Rate',
|
|
value: `${stats.bounceRate}%`,
|
|
sub: 'Exit rate',
|
|
icon: Users,
|
|
tab: 'analytics' as TabId,
|
|
gradient: 'from-pink-400/20 to-pink-400/5',
|
|
border: 'border-pink-400/20 dark:border-pink-400/10',
|
|
iconColor: 'text-pink-500',
|
|
tooltip: 'REAL DATA: Percentage of sessions with only 1 pageview. Lower is better.',
|
|
},
|
|
{
|
|
label: 'System',
|
|
value: 'Online',
|
|
sub: 'Operational',
|
|
icon: Shield,
|
|
tab: 'settings' as TabId,
|
|
gradient: 'from-teal-400/20 to-teal-400/5',
|
|
border: 'border-teal-400/20 dark:border-teal-400/10',
|
|
iconColor: 'text-teal-500',
|
|
pulse: true,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-stone-50 dark:bg-stone-950">
|
|
|
|
{/* Navbar */}
|
|
<div className="sticky top-0 z-50 bg-stone-50/90 dark:bg-stone-950/90 backdrop-blur-xl border-b border-stone-200 dark:border-stone-800">
|
|
{/* Gradient accent bar */}
|
|
<div className="h-0.5 bg-gradient-to-r from-emerald-400 via-sky-400 to-purple-400" />
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between h-14">
|
|
|
|
{/* Left: branding */}
|
|
<div className="flex items-center gap-4">
|
|
<Link
|
|
href="/"
|
|
className="flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-stone-50 transition-colors"
|
|
>
|
|
<Home size={16} />
|
|
<span className="font-mono text-xs font-black tracking-tighter">
|
|
dk<span className="text-red-500">0</span>.dev
|
|
</span>
|
|
</Link>
|
|
<div className="h-4 w-px bg-stone-300 dark:bg-stone-700" />
|
|
<span className="font-black text-xs uppercase tracking-[0.15em] text-stone-900 dark:text-stone-50">
|
|
Admin
|
|
</span>
|
|
</div>
|
|
|
|
{/* Center: desktop tabs */}
|
|
<div className="hidden md:flex items-center gap-1">
|
|
{navigation.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => setActiveTab(item.id)}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold uppercase tracking-[0.1em] transition-all duration-200 ${
|
|
activeTab === item.id
|
|
? 'bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900'
|
|
: 'text-stone-500 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-50 hover:bg-stone-100 dark:hover:bg-stone-900'
|
|
}`}
|
|
>
|
|
<item.icon size={13} />
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Right: user + logout + mobile toggle */}
|
|
<div className="flex items-center gap-3">
|
|
<span className="hidden sm:block text-xs text-stone-400 dark:text-stone-500 font-mono">
|
|
Dennis
|
|
</span>
|
|
<button
|
|
onClick={async () => {
|
|
try {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
sessionStorage.removeItem('admin_authenticated');
|
|
sessionStorage.removeItem('admin_session_token');
|
|
window.location.href = '/manage';
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
sessionStorage.removeItem('admin_authenticated');
|
|
sessionStorage.removeItem('admin_session_token');
|
|
window.location.href = '/manage';
|
|
}
|
|
}}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold uppercase tracking-[0.1em] text-stone-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/20 transition-all duration-200"
|
|
>
|
|
<LogOut size={13} />
|
|
<span className="hidden sm:inline">Logout</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
className="md:hidden p-2 rounded-xl text-stone-600 dark:text-stone-400 hover:bg-stone-100 dark:hover:bg-stone-900 transition-colors"
|
|
>
|
|
{mobileMenuOpen ? <X size={18} /> : <Menu size={18} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu */}
|
|
{mobileMenuOpen && (
|
|
<div className="md:hidden border-t border-stone-200 dark:border-stone-800 bg-stone-50 dark:bg-stone-950">
|
|
<div className="px-4 py-3 grid grid-cols-2 gap-2">
|
|
{navigation.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => {
|
|
setActiveTab(item.id);
|
|
setMobileMenuOpen(false);
|
|
}}
|
|
className={`flex items-center gap-2 px-4 py-3 rounded-2xl transition-all duration-200 text-left ${
|
|
activeTab === item.id
|
|
? 'bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900'
|
|
: 'text-stone-500 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-50 bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800'
|
|
}`}
|
|
>
|
|
<item.icon size={16} />
|
|
<div>
|
|
<div className="font-bold text-xs uppercase tracking-[0.1em]">{item.label}</div>
|
|
<div className="text-[10px] opacity-60">{item.description}</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Main content */}
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-8">
|
|
|
|
{/* Overview tab */}
|
|
{activeTab === 'overview' && (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<h1 className="text-4xl sm:text-5xl font-black tracking-tighter uppercase text-stone-900 dark:text-stone-50">
|
|
Dashboard<span className="text-emerald-500">.</span>
|
|
</h1>
|
|
<p className="text-stone-500 dark:text-stone-400 mt-1 text-sm">
|
|
Manage your portfolio and monitor performance
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats grid */}
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
|
{statCards.map((card) => (
|
|
<div
|
|
key={card.label}
|
|
className={`relative group bg-gradient-to-br ${card.gradient} border ${card.border} rounded-3xl p-5 cursor-pointer hover:scale-[1.02] active:scale-[0.98] transition-all duration-200`}
|
|
onClick={() => setActiveTab(card.tab)}
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-stone-500 dark:text-stone-400">
|
|
{card.label}
|
|
</p>
|
|
<card.icon size={15} className={card.iconColor} />
|
|
</div>
|
|
<p className="text-2xl font-black tracking-tighter text-stone-900 dark:text-stone-50">
|
|
{card.pulse ? (
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" />
|
|
{card.value}
|
|
</span>
|
|
) : card.value}
|
|
</p>
|
|
<p className={`text-[11px] font-medium mt-1 ${card.subColor ?? 'text-stone-500 dark:text-stone-400'}`}>
|
|
{card.sub}
|
|
</p>
|
|
|
|
{card.tooltip && (
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-stone-900/95 dark:bg-stone-800 text-stone-50 text-[10px] font-medium rounded-xl opacity-0 group-hover:opacity-100 transition-opacity whitespace-normal max-w-[200px] z-50 shadow-xl pointer-events-none text-center">
|
|
{card.tooltip}
|
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-2 h-2 bg-stone-900/95 dark:bg-stone-800 rotate-45" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Recent Activity + Quick Actions */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
{/* Recent Activity */}
|
|
<div className="lg:col-span-2 bg-white dark:bg-stone-900 border border-stone-100 dark:border-stone-800 rounded-3xl p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-black tracking-tight uppercase text-stone-900 dark:text-stone-50">
|
|
Recent Activity
|
|
</h2>
|
|
<button
|
|
onClick={() => loadAllData()}
|
|
className="text-xs font-bold uppercase tracking-[0.1em] text-stone-400 hover:text-stone-900 dark:hover:text-stone-50 px-3 py-1.5 bg-stone-50 dark:bg-stone-800 rounded-xl border border-stone-200 dark:border-stone-700 transition-all"
|
|
>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-3">
|
|
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-stone-400">Projects</h3>
|
|
{projects.slice(0, 3).map((project) => (
|
|
<div
|
|
key={project.id}
|
|
className="flex items-start gap-3 p-3 bg-stone-50 dark:bg-stone-800/50 border border-stone-100 dark:border-stone-700 rounded-2xl hover:border-stone-300 dark:hover:border-stone-600 transition-all cursor-pointer"
|
|
onClick={() => setActiveTab('projects')}
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-stone-900 dark:text-stone-50 font-bold text-sm truncate">{project.title}</p>
|
|
<p className="text-stone-400 text-xs mt-0.5">{project.analytics?.views || 0} views</p>
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold ${project.published ? 'bg-emerald-100 dark:bg-emerald-950/30 text-emerald-700 dark:text-emerald-400' : 'bg-amber-100 dark:bg-amber-950/30 text-amber-700 dark:text-amber-400'}`}>
|
|
{project.published ? 'Live' : 'Draft'}
|
|
</span>
|
|
{project.featured && (
|
|
<span className="px-2 py-0.5 bg-stone-100 dark:bg-stone-700 text-stone-600 dark:text-stone-300 rounded-full text-[10px] font-bold">Featured</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{projects.length === 0 && (
|
|
<p className="text-stone-400 text-xs py-4">No projects yet</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-stone-400">Messages</h3>
|
|
{emails.slice(0, 3).map((email, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center gap-3 p-3 bg-stone-50 dark:bg-stone-800/50 border border-stone-100 dark:border-stone-700 rounded-2xl hover:border-stone-300 dark:hover:border-stone-600 transition-all cursor-pointer"
|
|
onClick={() => setActiveTab('emails')}
|
|
>
|
|
<div className="w-8 h-8 bg-gradient-to-br from-purple-400/20 to-purple-400/5 border border-purple-400/20 rounded-xl flex items-center justify-center flex-shrink-0">
|
|
<Mail size={13} className="text-purple-500" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-stone-900 dark:text-stone-50 font-bold text-sm truncate">
|
|
{email.name as string}
|
|
</p>
|
|
<p className="text-stone-400 text-xs truncate">{(email.subject as string) || 'No subject'}</p>
|
|
</div>
|
|
{!(email.read as boolean) && (
|
|
<div className="w-2 h-2 bg-red-500 rounded-full flex-shrink-0" />
|
|
)}
|
|
</div>
|
|
))}
|
|
{emails.length === 0 && (
|
|
<p className="text-stone-400 text-xs py-4">No messages yet</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="bg-white dark:bg-stone-900 border border-stone-100 dark:border-stone-800 rounded-3xl p-6">
|
|
<h2 className="text-lg font-black tracking-tight uppercase text-stone-900 dark:text-stone-50 mb-6">
|
|
Quick Actions
|
|
</h2>
|
|
<div className="space-y-3">
|
|
{[
|
|
{ label: 'Ghost Editor', sub: 'Professional writing tool', icon: Plus, action: () => window.location.href = '/editor', color: 'from-emerald-400/20 to-emerald-400/5 border-emerald-400/20' },
|
|
{ label: 'View Messages', sub: `${stats.unreadEmails} unread`, icon: Mail, action: () => setActiveTab('emails'), color: 'from-purple-400/20 to-purple-400/5 border-purple-400/20' },
|
|
{ label: 'Analytics', sub: 'View detailed stats', icon: TrendingUp, action: () => setActiveTab('analytics'), color: 'from-sky-400/20 to-sky-400/5 border-sky-400/20' },
|
|
{ label: 'Settings', sub: 'System configuration', icon: Settings, action: () => setActiveTab('settings'), color: 'from-stone-400/20 to-stone-400/5 border-stone-400/20' },
|
|
].map((item) => (
|
|
<button
|
|
key={item.label}
|
|
onClick={item.action}
|
|
className={`w-full flex items-center gap-3 p-3 bg-gradient-to-r ${item.color} border rounded-2xl hover:scale-[1.02] active:scale-[0.98] transition-all duration-200 text-left`}
|
|
>
|
|
<div className="w-9 h-9 bg-white dark:bg-stone-800 rounded-xl border border-stone-100 dark:border-stone-700 flex items-center justify-center flex-shrink-0">
|
|
<item.icon size={15} className="text-stone-600 dark:text-stone-300" />
|
|
</div>
|
|
<div>
|
|
<p className="text-stone-900 dark:text-stone-50 font-bold text-sm">{item.label}</p>
|
|
<p className="text-stone-400 text-xs">{item.sub}</p>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'projects' && (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-4xl sm:text-5xl font-black tracking-tighter uppercase text-stone-900 dark:text-stone-50">
|
|
Projects<span className="text-emerald-500">.</span>
|
|
</h1>
|
|
<p className="text-stone-500 dark:text-stone-400 mt-1 text-sm">Manage your portfolio projects</p>
|
|
</div>
|
|
<ProjectManager projects={projects} onProjectsChange={loadProjects} />
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'emails' && (
|
|
<EmailManager />
|
|
)}
|
|
|
|
{activeTab === 'analytics' && (
|
|
<AnalyticsDashboard isAuthenticated={isAuthenticated} />
|
|
)}
|
|
|
|
{activeTab === 'content' && (
|
|
<ContentManager />
|
|
)}
|
|
|
|
{activeTab === 'settings' && (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<h1 className="text-4xl sm:text-5xl font-black tracking-tighter uppercase text-stone-900 dark:text-stone-50">
|
|
Settings<span className="text-emerald-500">.</span>
|
|
</h1>
|
|
<p className="text-stone-500 dark:text-stone-400 mt-1 text-sm">Manage system configuration and preferences</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div className="bg-white dark:bg-stone-900 border border-stone-100 dark:border-stone-800 rounded-3xl p-6">
|
|
<h2 className="text-lg font-black tracking-tight uppercase text-stone-900 dark:text-stone-50 mb-2">
|
|
Import / Export
|
|
</h2>
|
|
<p className="text-stone-400 text-sm mb-6">Backup and restore your portfolio data</p>
|
|
<ImportExport />
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-stone-900 border border-stone-100 dark:border-stone-800 rounded-3xl p-6">
|
|
<h2 className="text-lg font-black tracking-tight uppercase text-stone-900 dark:text-stone-50 mb-6">
|
|
System Status
|
|
</h2>
|
|
<div className="space-y-3">
|
|
{[
|
|
{ label: 'Database', color: 'bg-emerald-400/20 border-emerald-400/20', dot: 'bg-emerald-500', text: 'text-emerald-600 dark:text-emerald-400' },
|
|
{ label: 'Redis Cache', color: 'bg-emerald-400/20 border-emerald-400/20', dot: 'bg-emerald-500', text: 'text-emerald-600 dark:text-emerald-400' },
|
|
{ label: 'API Services', color: 'bg-emerald-400/20 border-emerald-400/20', dot: 'bg-emerald-500', text: 'text-emerald-600 dark:text-emerald-400' },
|
|
].map((item) => (
|
|
<div
|
|
key={item.label}
|
|
className={`flex items-center justify-between p-4 bg-gradient-to-r ${item.color} border rounded-2xl`}
|
|
>
|
|
<span className="text-stone-600 dark:text-stone-300 font-medium text-sm">{item.label}</span>
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-2 h-2 ${item.dot} rounded-full animate-pulse`} />
|
|
<span className={`${item.text} font-bold text-sm`}>Online</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ModernAdminDashboard;
|