feat: redesign admin panel to match Liquid Editorial Bento design system
- 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>
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { Lock, Loader2 } from 'lucide-react';
|
import { Lock, Loader2 } from 'lucide-react';
|
||||||
import ModernAdminDashboard from '@/components/ModernAdminDashboard';
|
import ModernAdminDashboard from '@/components/ModernAdminDashboard';
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ const AdminPage = () => {
|
|||||||
// Check if user is locked out
|
// Check if user is locked out
|
||||||
const checkLockout = useCallback(() => {
|
const checkLockout = useCallback(() => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const lockoutData = localStorage.getItem('admin_lockout');
|
const lockoutData = localStorage.getItem('admin_lockout');
|
||||||
if (lockoutData) {
|
if (lockoutData) {
|
||||||
@@ -103,11 +102,11 @@ const AdminPage = () => {
|
|||||||
try {
|
try {
|
||||||
const sessionToken = sessionStorage.getItem('admin_session_token');
|
const sessionToken = sessionStorage.getItem('admin_session_token');
|
||||||
if (!sessionToken) {
|
if (!sessionToken) {
|
||||||
setAuthState(prev => ({
|
setAuthState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
showLogin: true,
|
showLogin: true,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,38 +117,38 @@ const AdminPage = () => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRF-Token': authState.csrfToken
|
'X-CSRF-Token': authState.csrfToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
sessionToken,
|
sessionToken,
|
||||||
csrfToken: authState.csrfToken
|
csrfToken: authState.csrfToken
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok && data.valid) {
|
if (response.ok && data.valid) {
|
||||||
setAuthState(prev => ({
|
setAuthState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
showLogin: false,
|
showLogin: false,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}));
|
}));
|
||||||
sessionStorage.setItem('admin_authenticated', 'true');
|
sessionStorage.setItem('admin_authenticated', 'true');
|
||||||
} else {
|
} else {
|
||||||
sessionStorage.removeItem('admin_authenticated');
|
sessionStorage.removeItem('admin_authenticated');
|
||||||
sessionStorage.removeItem('admin_session_token');
|
sessionStorage.removeItem('admin_session_token');
|
||||||
setAuthState(prev => ({
|
setAuthState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
showLogin: true,
|
showLogin: true,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setAuthState(prev => ({
|
setAuthState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
showLogin: true,
|
showLogin: true,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [authState.csrfToken]);
|
}, [authState.csrfToken]);
|
||||||
@@ -158,13 +157,13 @@ const AdminPage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
if (checkLockout()) return;
|
if (checkLockout()) return;
|
||||||
|
|
||||||
const token = await fetchCSRFToken();
|
const token = await fetchCSRFToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
setAuthState(prev => ({ ...prev, csrfToken: token }));
|
setAuthState(prev => ({ ...prev, csrfToken: token }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, [checkLockout, fetchCSRFToken]);
|
}, [checkLockout, fetchCSRFToken]);
|
||||||
|
|
||||||
@@ -178,7 +177,7 @@ const AdminPage = () => {
|
|||||||
// Handle login form submission
|
// Handle login form submission
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!authState.password.trim() || authState.isLoading) return;
|
if (!authState.password.trim() || authState.isLoading) return;
|
||||||
|
|
||||||
setAuthState(prev => ({ ...prev, isLoading: true, error: '' }));
|
setAuthState(prev => ({ ...prev, isLoading: true, error: '' }));
|
||||||
@@ -259,10 +258,12 @@ const AdminPage = () => {
|
|||||||
// Loading state
|
// Loading state
|
||||||
if (authState.isLoading) {
|
if (authState.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3]">
|
<div className="min-h-screen flex items-center justify-center bg-stone-50 dark:bg-stone-950">
|
||||||
<div className="text-center">
|
<div className="text-center space-y-4">
|
||||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-[#795548]" />
|
<div className="font-mono text-sm font-black tracking-tighter text-stone-900 dark:text-stone-50">
|
||||||
<p className="text-[#5d4037]">Loading...</p>
|
dk<span className="text-red-500">0</span>.dev
|
||||||
|
</div>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin mx-auto text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -271,26 +272,38 @@ const AdminPage = () => {
|
|||||||
// Lockout state
|
// Lockout state
|
||||||
if (authState.isLocked) {
|
if (authState.isLocked) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3]">
|
<div className="min-h-screen flex items-center justify-center bg-stone-50 dark:bg-stone-950 px-6">
|
||||||
<div className="text-center">
|
<div className="w-full max-w-sm">
|
||||||
<div className="w-16 h-16 bg-[#fecaca] rounded-2xl flex items-center justify-center mx-auto mb-6">
|
<div className="bg-white dark:bg-stone-900 rounded-[2.5rem] border border-stone-200 dark:border-stone-800 overflow-hidden shadow-2xl">
|
||||||
<Lock className="w-8 h-8 text-[#d84315]" />
|
<div className="h-0.5 bg-gradient-to-r from-red-500 via-orange-400 to-red-400" />
|
||||||
|
<div className="p-10 text-center">
|
||||||
|
<div className="w-14 h-14 bg-red-50 dark:bg-red-950/30 rounded-[1.25rem] flex items-center justify-center mx-auto mb-6 border border-red-200 dark:border-red-900">
|
||||||
|
<Lock className="w-6 h-6 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<p className="font-mono text-[10px] font-black uppercase tracking-[0.2em] text-stone-400 mb-4">
|
||||||
|
dk<span className="text-red-500">0</span>.dev · admin
|
||||||
|
</p>
|
||||||
|
<h1 className="text-2xl font-black tracking-tighter uppercase text-stone-900 dark:text-stone-50 mb-2">
|
||||||
|
Account Locked
|
||||||
|
</h1>
|
||||||
|
<p className="text-stone-500 dark:text-stone-400 text-sm leading-relaxed mb-8">
|
||||||
|
Too many failed attempts. Please try again in 15 minutes.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('admin_lockout');
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
className="px-8 py-3 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:scale-105 active:scale-95 transition-all"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-[#3e2723] mb-2">Account Locked</h2>
|
|
||||||
<p className="text-[#5d4037]">Too many failed attempts. Please try again in 15 minutes.</p>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem('admin_lockout');
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
className="mt-4 px-6 py-2 bg-[#5d4037] text-[#faf8f3] rounded-xl hover:bg-[#3e2723] transition-colors"
|
|
||||||
>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -299,70 +312,84 @@ const AdminPage = () => {
|
|||||||
// Login form
|
// Login form
|
||||||
if (authState.showLogin || !authState.isAuthenticated) {
|
if (authState.showLogin || !authState.isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[#faf8f3] z-0">
|
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-stone-50 dark:bg-stone-950 px-6">
|
||||||
|
{/* Liquid ambient blobs */}
|
||||||
<motion.div
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
<div className="absolute top-[5%] left-[5%] w-[50vw] h-[50vw] bg-liquid-mint rounded-full blur-[140px] opacity-20" />
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
<div className="absolute bottom-[5%] right-[5%] w-[40vw] h-[40vw] bg-liquid-purple rounded-full blur-[120px] opacity-15" />
|
||||||
className="w-full max-w-md p-6"
|
</div>
|
||||||
>
|
|
||||||
<div className="bg-[#fffcf5] backdrop-blur-xl rounded-3xl p-8 border border-[#d7ccc8] shadow-2xl relative z-10">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<div className="w-16 h-16 bg-[#efebe9] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm border border-[#d7ccc8]">
|
|
||||||
<Lock className="w-6 h-6 text-[#5d4037]" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold text-[#3e2723] mb-2 tracking-tight">Admin Access</h1>
|
|
||||||
<p className="text-[#5d4037]">Enter your password to continue</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleLogin} className="space-y-5">
|
<div className="relative z-10 w-full max-w-sm animate-[fadeIn_0.4s_ease-out]">
|
||||||
<div>
|
<div className="bg-white dark:bg-stone-900 rounded-[2.5rem] border border-stone-200 dark:border-stone-800 overflow-hidden shadow-2xl">
|
||||||
<div className="relative">
|
<div className="h-0.5 bg-gradient-to-r from-emerald-400 via-sky-400 to-purple-400" />
|
||||||
<input
|
|
||||||
type={authState.showPassword ? 'text' : 'password'}
|
<div className="p-10">
|
||||||
value={authState.password}
|
<div className="text-center mb-8">
|
||||||
onChange={(e) => setAuthState(prev => ({ ...prev, password: e.target.value }))}
|
<p className="font-mono text-[10px] font-black uppercase tracking-[0.2em] text-stone-400 mb-5">
|
||||||
placeholder="Enter password"
|
dk<span className="text-red-500">0</span>.dev · admin
|
||||||
className="w-full px-4 py-3.5 bg-white border border-[#d7ccc8] rounded-xl text-[#3e2723] placeholder:text-[#a1887f] focus:outline-none focus:ring-2 focus:ring-[#bcaaa4] focus:border-[#5d4037] transition-all shadow-sm"
|
</p>
|
||||||
disabled={authState.isLoading}
|
<div className="w-14 h-14 bg-stone-100 dark:bg-stone-800 rounded-[1.25rem] flex items-center justify-center mx-auto mb-5 border border-stone-200 dark:border-stone-700">
|
||||||
/>
|
<Lock className="w-6 h-6 text-stone-700 dark:text-stone-300" />
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAuthState(prev => ({ ...prev, showPassword: !prev.showPassword }))}
|
|
||||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-[#a1887f] hover:text-[#5d4037] p-1"
|
|
||||||
>
|
|
||||||
{authState.showPassword ? '👁️' : '👁️🗨️'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{authState.error && (
|
<h1 className="text-3xl font-black tracking-tighter uppercase text-stone-900 dark:text-stone-50 mb-1">
|
||||||
<motion.p
|
Admin Access
|
||||||
initial={{ opacity: 0, y: -5 }}
|
</h1>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<p className="text-stone-500 dark:text-stone-400 text-sm">
|
||||||
className="mt-2 text-[#d84315] text-sm font-medium flex items-center"
|
Enter your password to continue
|
||||||
>
|
</p>
|
||||||
<span className="w-1.5 h-1.5 bg-[#d84315] rounded-full mr-2" />
|
|
||||||
{authState.error}
|
|
||||||
</motion.p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
type="submit"
|
<div>
|
||||||
disabled={authState.isLoading || !authState.password}
|
<div className="relative">
|
||||||
className="w-full bg-[#5d4037] text-[#faf8f3] py-3.5 px-6 rounded-xl font-semibold text-lg hover:bg-[#3e2723] focus:outline-none focus:ring-2 focus:ring-[#bcaaa4] focus:ring-offset-2 focus:ring-offset-white disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg flex items-center justify-center"
|
<input
|
||||||
>
|
type={authState.showPassword ? 'text' : 'password'}
|
||||||
{authState.isLoading ? (
|
value={authState.password}
|
||||||
<div className="flex items-center justify-center space-x-2">
|
onChange={(e) => setAuthState(prev => ({ ...prev, password: e.target.value }))}
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
placeholder="Password"
|
||||||
<span className="text-[#faf8f3]">Authenticating...</span>
|
className="w-full px-5 py-4 bg-stone-50 dark:bg-stone-950/50 border border-stone-200 dark:border-stone-700 rounded-2xl text-stone-900 dark:text-stone-50 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:border-transparent transition-all"
|
||||||
|
disabled={authState.isLoading}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAuthState(prev => ({ ...prev, showPassword: !prev.showPassword }))}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-stone-400 hover:text-stone-700 dark:hover:text-stone-200 p-1 transition-colors"
|
||||||
|
>
|
||||||
|
{authState.showPassword ? '👁️' : '👁️🗨️'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
{authState.error && (
|
||||||
<span className="text-[#faf8f3]">Sign In</span>
|
<p className="mt-2 text-red-500 text-sm font-medium flex items-center gap-2">
|
||||||
)}
|
<span className="w-1.5 h-1.5 bg-red-500 rounded-full flex-shrink-0" />
|
||||||
</button>
|
{authState.error}
|
||||||
</form>
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={authState.isLoading || !authState.password}
|
||||||
|
className="w-full bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 py-4 rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{authState.isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Authenticating
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Sign In'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
|
{authState.attempts > 0 && (
|
||||||
|
<p className="text-center text-xs text-stone-400 mt-4">
|
||||||
|
{5 - authState.attempts} attempt{5 - authState.attempts !== 1 ? 's' : ''} remaining
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -375,4 +402,4 @@ const AdminPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminPage;
|
export default AdminPage;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import {
|
import {
|
||||||
Mail,
|
Mail,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -21,23 +20,23 @@ import dynamic from 'next/dynamic';
|
|||||||
|
|
||||||
const EmailManager = dynamic(
|
const EmailManager = dynamic(
|
||||||
() => import('./EmailManager').then((m) => m.EmailManager),
|
() => import('./EmailManager').then((m) => m.EmailManager),
|
||||||
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading emails…</div> }
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading emails…</div> }
|
||||||
);
|
);
|
||||||
const AnalyticsDashboard = dynamic(
|
const AnalyticsDashboard = dynamic(
|
||||||
() => import('./AnalyticsDashboard').then((m) => m.default),
|
() => import('./AnalyticsDashboard').then((m) => m.default),
|
||||||
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading analytics…</div> }
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading analytics…</div> }
|
||||||
);
|
);
|
||||||
const ImportExport = dynamic(
|
const ImportExport = dynamic(
|
||||||
() => import('./ImportExport').then((m) => m.default),
|
() => import('./ImportExport').then((m) => m.default),
|
||||||
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading tools…</div> }
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading tools…</div> }
|
||||||
);
|
);
|
||||||
const ProjectManager = dynamic(
|
const ProjectManager = dynamic(
|
||||||
() => import('./ProjectManager').then((m) => m.ProjectManager),
|
() => import('./ProjectManager').then((m) => m.ProjectManager),
|
||||||
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading projects…</div> }
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading projects…</div> }
|
||||||
);
|
);
|
||||||
const ContentManager = dynamic(
|
const ContentManager = dynamic(
|
||||||
() => import('./ContentManager').then((m) => m.default),
|
() => import('./ContentManager').then((m) => m.default),
|
||||||
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading content…</div> }
|
{ ssr: false, loading: () => <div className="p-6 text-stone-400">Loading content…</div> }
|
||||||
);
|
);
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
@@ -69,8 +68,10 @@ interface ModernAdminDashboardProps {
|
|||||||
isAuthenticated?: boolean;
|
isAuthenticated?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TabId = 'overview' | 'projects' | 'emails' | 'analytics' | 'content' | 'settings';
|
||||||
|
|
||||||
const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthenticated = true }) => {
|
const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthenticated = true }) => {
|
||||||
const [activeTab, setActiveTab] = useState<'overview' | 'projects' | 'emails' | 'analytics' | 'content' | 'settings'>('overview');
|
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -180,7 +181,6 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
|||||||
totalViews: ((analytics?.overview as Record<string, unknown>)?.totalViews as number) || (analytics?.totalViews as number) || projects.reduce((sum, p) => sum + (p.analytics?.views || 0), 0),
|
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,
|
unreadEmails: emails.filter(e => !(e.read as boolean)).length,
|
||||||
avgPerformance: (() => {
|
avgPerformance: (() => {
|
||||||
// Only show real performance data, not defaults
|
|
||||||
const projectsWithPerf = projects.filter(p => {
|
const projectsWithPerf = projects.filter(p => {
|
||||||
const perf = p.performance as Record<string, unknown> || {};
|
const perf = p.performance as Record<string, unknown> || {};
|
||||||
return (perf.lighthouse as number || 0) > 0;
|
return (perf.lighthouse as number || 0) > 0;
|
||||||
@@ -198,7 +198,6 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prioritize the data needed for the initial dashboard render
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
await Promise.all([loadProjects(), loadSystemStats()]);
|
await Promise.all([loadProjects(), loadSystemStats()]);
|
||||||
|
|
||||||
@@ -218,467 +217,424 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
|||||||
}, [loadProjects, loadSystemStats, loadAnalytics, loadEmails]);
|
}, [loadProjects, loadSystemStats, loadAnalytics, loadEmails]);
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ id: 'overview', label: 'Dashboard', icon: Home, color: 'blue', description: 'Overview & Statistics' },
|
{ id: 'overview' as TabId, label: 'Dashboard', icon: Home, description: 'Overview & Statistics' },
|
||||||
{ id: 'projects', label: 'Projects', icon: Database, color: 'green', description: 'Manage Projects' },
|
{ id: 'projects' as TabId, label: 'Projects', icon: Database, description: 'Manage Projects' },
|
||||||
{ id: 'emails', label: 'Emails', icon: Mail, color: 'purple', description: 'Email Management' },
|
{ id: 'emails' as TabId, label: 'Emails', icon: Mail, description: 'Email Management' },
|
||||||
{ id: 'analytics', label: 'Analytics', icon: Activity, color: 'orange', description: 'Site Analytics' },
|
{ id: 'analytics' as TabId, label: 'Analytics', icon: Activity, description: 'Site Analytics' },
|
||||||
{ id: 'content', label: 'Content', icon: Shield, color: 'teal', description: 'Texts, pages & localization' },
|
{ id: 'content' as TabId, label: 'Content', icon: Shield, description: 'Texts, pages & localization' },
|
||||||
{ id: 'settings', label: 'Settings', icon: Settings, color: 'gray', description: 'System Settings' }
|
{ 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 (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen bg-stone-50 dark:bg-stone-950">
|
||||||
{/* Animated Background - same as main site */}
|
|
||||||
<div className="fixed inset-0 animated-bg"></div>
|
|
||||||
|
|
||||||
{/* Admin Navbar - Horizontal Navigation */}
|
{/* Navbar */}
|
||||||
<div className="relative z-10">
|
<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">
|
||||||
<div className="admin-glass border-b border-white/20 sticky top-0">
|
{/* Gradient accent bar */}
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="h-0.5 bg-gradient-to-r from-emerald-400 via-sky-400 to-purple-400" />
|
||||||
<div className="flex items-center justify-between h-16">
|
|
||||||
{/* Left side - Logo and Admin Panel */}
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="flex items-center space-x-2 text-stone-900 hover:text-black transition-colors"
|
|
||||||
>
|
|
||||||
<Home size={20} className="text-stone-600" />
|
|
||||||
<span className="font-medium text-stone-900">Portfolio</span>
|
|
||||||
</Link>
|
|
||||||
<div className="h-6 w-px bg-stone-300" />
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Shield size={20} className="text-stone-600" />
|
|
||||||
<span className="text-stone-900 font-semibold">Admin Panel</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Center - Desktop Navigation */}
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="hidden md:flex items-center space-x-1">
|
<div className="flex items-center justify-between h-14">
|
||||||
{navigation.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => setActiveTab(item.id as 'overview' | 'projects' | 'emails' | 'analytics' | 'content' | 'settings')}
|
|
||||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 ${
|
|
||||||
activeTab === item.id
|
|
||||||
? 'bg-stone-100 text-stone-900 font-medium shadow-sm border border-stone-200'
|
|
||||||
: 'text-stone-500 hover:text-stone-800 hover:bg-stone-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<item.icon size={16} className={activeTab === item.id ? 'text-stone-800' : 'text-stone-400'} />
|
|
||||||
<span className="text-sm">{item.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side - User info and Logout */}
|
{/* Left: branding */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="hidden sm:block text-sm text-stone-500">
|
<Link
|
||||||
Welcome, <span className="text-stone-800 font-semibold">Dennis</span>
|
href="/"
|
||||||
</div>
|
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
|
<button
|
||||||
onClick={async () => {
|
key={item.id}
|
||||||
try {
|
onClick={() => setActiveTab(item.id)}
|
||||||
await fetch('/api/auth/logout', { method: 'POST' });
|
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 ${
|
||||||
sessionStorage.removeItem('admin_authenticated');
|
activeTab === item.id
|
||||||
sessionStorage.removeItem('admin_session_token');
|
? 'bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900'
|
||||||
window.location.href = '/manage';
|
: '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'
|
||||||
} catch (error) {
|
}`}
|
||||||
console.error('Logout failed:', error);
|
|
||||||
// Force logout anyway
|
|
||||||
sessionStorage.removeItem('admin_authenticated');
|
|
||||||
sessionStorage.removeItem('admin_session_token');
|
|
||||||
window.location.href = '/manage';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex items-center space-x-2 px-3 py-2 rounded-lg hover:bg-red-50 text-stone-500 hover:text-red-600 transition-all duration-200 border border-transparent hover:border-red-100"
|
|
||||||
>
|
>
|
||||||
<LogOut size={16} />
|
<item.icon size={13} />
|
||||||
<span className="hidden sm:inline text-sm font-medium">Logout</span>
|
{item.label}
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu button */}
|
{/* Right: user + logout + mobile toggle */}
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
<span className="hidden sm:block text-xs text-stone-400 dark:text-stone-500 font-mono">
|
||||||
className="md:hidden flex items-center justify-center p-2 rounded-lg text-stone-600 hover:bg-stone-100 transition-colors"
|
Dennis
|
||||||
>
|
</span>
|
||||||
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
|
<button
|
||||||
</button>
|
onClick={async () => {
|
||||||
</div>
|
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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile Navigation Menu */}
|
{/* Mobile menu */}
|
||||||
<AnimatePresence>
|
{mobileMenuOpen && (
|
||||||
{mobileMenuOpen && (
|
<div className="md:hidden border-t border-stone-200 dark:border-stone-800 bg-stone-50 dark:bg-stone-950">
|
||||||
<motion.div
|
<div className="px-4 py-3 grid grid-cols-2 gap-2">
|
||||||
initial={{ opacity: 0, height: 0 }}
|
{navigation.map((item) => (
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
<button
|
||||||
exit={{ opacity: 0, height: 0 }}
|
key={item.id}
|
||||||
className="md:hidden border-t border-stone-200 bg-white"
|
onClick={() => {
|
||||||
>
|
setActiveTab(item.id);
|
||||||
<div className="px-4 py-4 space-y-2">
|
setMobileMenuOpen(false);
|
||||||
{navigation.map((item) => (
|
}}
|
||||||
|
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
|
<button
|
||||||
key={item.id}
|
key={item.label}
|
||||||
onClick={() => {
|
onClick={item.action}
|
||||||
setActiveTab(item.id as 'overview' | 'projects' | 'emails' | 'analytics' | 'content' | 'settings');
|
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`}
|
||||||
setMobileMenuOpen(false);
|
|
||||||
}}
|
|
||||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200 ${
|
|
||||||
activeTab === item.id
|
|
||||||
? 'bg-stone-100 text-stone-900 shadow-sm border border-stone-200'
|
|
||||||
: 'text-stone-500 hover:text-stone-800 hover:bg-stone-50'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<item.icon size={18} className={activeTab === item.id ? 'text-stone-800' : 'text-stone-400'} />
|
<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">
|
||||||
<div className="text-left">
|
<item.icon size={15} className="text-stone-600 dark:text-stone-300" />
|
||||||
<div className="font-medium text-sm">{item.label}</div>
|
</div>
|
||||||
<div className="text-xs opacity-70">{item.description}</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>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</AnimatePresence>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Main Content - Full Width Horizontal Layout */}
|
{activeTab === 'projects' && (
|
||||||
<div className="px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-6 lg:py-8">
|
<div className="space-y-6">
|
||||||
{/* Content */}
|
<div>
|
||||||
<AnimatePresence mode="wait">
|
<h1 className="text-4xl sm:text-5xl font-black tracking-tighter uppercase text-stone-900 dark:text-stone-50">
|
||||||
<motion.div
|
Projects<span className="text-emerald-500">.</span>
|
||||||
key={activeTab}
|
</h1>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<p className="text-stone-500 dark:text-stone-400 mt-1 text-sm">Manage your portfolio projects</p>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
</div>
|
||||||
exit={{ opacity: 0, y: -20 }}
|
<ProjectManager projects={projects} onProjectsChange={loadProjects} />
|
||||||
transition={{ duration: 0.3 }}
|
</div>
|
||||||
>
|
)}
|
||||||
{activeTab === 'overview' && (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-stone-900">Admin Dashboard</h1>
|
|
||||||
<p className="text-stone-500 text-lg">Manage your portfolio and monitor performance</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Grid - Mobile: 2x3, Desktop: 6x1 horizontal */}
|
{activeTab === 'emails' && (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-3 md:gap-6">
|
<EmailManager />
|
||||||
<div
|
)}
|
||||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none group relative"
|
|
||||||
onClick={() => setActiveTab('projects')}
|
{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`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col space-y-2">
|
<span className="text-stone-600 dark:text-stone-300 font-medium text-sm">{item.label}</span>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Projects</p>
|
<div className={`w-2 h-2 ${item.dot} rounded-full animate-pulse`} />
|
||||||
<Database size={20} className="text-stone-400" />
|
<span className={`${item.text} font-bold text-sm`}>Online</span>
|
||||||
</div>
|
|
||||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.totalProjects}</p>
|
|
||||||
<p className="text-stone-600 text-xs font-medium">{stats.publishedProjects} published</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-stone-900/95 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-normal max-w-xs z-50 shadow-xl backdrop-blur-sm pointer-events-none">
|
|
||||||
✅ REAL DATA: Total projects in your portfolio from the database. Shows published vs unpublished count.
|
|
||||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-2 h-2 bg-stone-900/95 rotate-45"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<div
|
|
||||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none group relative"
|
|
||||||
onClick={() => setActiveTab('analytics')}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Page Views</p>
|
|
||||||
<Activity size={20} className="text-stone-400" />
|
|
||||||
</div>
|
|
||||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.totalViews.toLocaleString()}</p>
|
|
||||||
<p className="text-stone-600 text-xs font-medium">{stats.totalUsers} users</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-stone-900/95 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-normal max-w-xs z-50 shadow-xl backdrop-blur-sm pointer-events-none">
|
|
||||||
✅ REAL DATA: Total page views from PageView table (last 30 days). Each visit is tracked with IP, user agent, and timestamp. Users = unique IP addresses.
|
|
||||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-2 h-2 bg-stone-900/95 rotate-45"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
|
||||||
onClick={() => setActiveTab('emails')}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Messages</p>
|
|
||||||
<Mail size={20} className="text-stone-400" />
|
|
||||||
</div>
|
|
||||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{emails.length}</p>
|
|
||||||
<p className="text-red-500 text-xs font-medium">{stats.unreadEmails} unread</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none group relative"
|
|
||||||
onClick={() => setActiveTab('analytics')}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Performance</p>
|
|
||||||
<TrendingUp size={20} className="text-stone-400" />
|
|
||||||
</div>
|
|
||||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.avgPerformance || 'N/A'}</p>
|
|
||||||
<p className="text-stone-600 text-xs font-medium">Lighthouse Score</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-stone-900/95 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-normal max-w-xs z-50 shadow-xl backdrop-blur-sm pointer-events-none">
|
|
||||||
{stats.avgPerformance > 0
|
|
||||||
? "✅ REAL DATA: Average Lighthouse score (0-100) calculated from real Web Vitals (LCP, FCP, CLS, FID, TTFB) collected from actual page visits. Only averages projects with real performance data."
|
|
||||||
: "No performance data yet. Scores appear after visitors load pages and Web Vitals are tracked."}
|
|
||||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-2 h-2 bg-stone-900/95 rotate-45"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none group relative"
|
|
||||||
onClick={() => setActiveTab('analytics')}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Bounce Rate</p>
|
|
||||||
<Users size={20} className="text-stone-400" />
|
|
||||||
</div>
|
|
||||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.bounceRate}%</p>
|
|
||||||
<p className="text-stone-600 text-xs font-medium">Exit rate</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-stone-900/95 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-normal max-w-xs z-50 shadow-xl backdrop-blur-sm pointer-events-none">
|
|
||||||
✅ REAL DATA: Percentage of sessions with only 1 pageview (calculated from PageView records grouped by IP). Lower is better. Shows how many visitors leave after viewing just one page.
|
|
||||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-2 h-2 bg-stone-900/95 rotate-45"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
|
||||||
onClick={() => setActiveTab('settings')}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-stone-500 text-xs md:text-sm font-medium">System</p>
|
|
||||||
<Shield size={20} className="text-stone-400" />
|
|
||||||
</div>
|
|
||||||
<p className="text-xl md:text-2xl font-bold text-stone-900">Online</p>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
|
||||||
<p className="text-stone-600 text-xs font-medium">Operational</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Activity & Quick Actions - Mobile: vertical, Desktop: horizontal */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
{/* Recent Activity */}
|
|
||||||
<div className="admin-glass-card p-6 rounded-xl md:col-span-2">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h2 className="text-xl font-bold text-stone-900">Recent Activity</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => loadAllData()}
|
|
||||||
className="text-stone-500 hover:text-stone-800 text-sm font-medium px-3 py-1 bg-stone-100 rounded-lg transition-colors border border-stone-200"
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile: vertical stack, Desktop: horizontal columns */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-wider">Projects</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{projects.slice(0, 3).map((project) => (
|
|
||||||
<div key={project.id} className="flex items-start space-x-3 p-4 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('projects')}>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-stone-800 font-medium text-sm truncate">{project.title}</p>
|
|
||||||
<p className="text-stone-500 text-xs">{project.published ? 'Published' : 'Draft'} • {project.analytics?.views || 0} views</p>
|
|
||||||
<div className="flex items-center space-x-2 mt-2">
|
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${project.published ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}`}>
|
|
||||||
{project.published ? 'Live' : 'Draft'}
|
|
||||||
</span>
|
|
||||||
{project.featured && (
|
|
||||||
<span className="px-2 py-1 bg-stone-200 text-stone-700 rounded-full text-xs font-medium">Featured</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-wider">Messages</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{emails.slice(0, 3).map((email, index) => (
|
|
||||||
<div key={index} className="flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('emails')}>
|
|
||||||
<div className="w-8 h-8 bg-stone-200 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
||||||
<Mail size={14} className="text-stone-600" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-stone-800 font-medium text-sm truncate">From {email.name as string}</p>
|
|
||||||
<p className="text-stone-500 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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<div className="admin-glass-card p-6 rounded-xl">
|
|
||||||
<h2 className="text-xl font-bold text-stone-900 mb-6">Quick Actions</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.href = '/editor'}
|
|
||||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
|
||||||
<Plus size={18} className="text-stone-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-stone-800 font-medium text-sm">Ghost Editor</p>
|
|
||||||
<p className="text-stone-500 text-xs">Professional writing tool</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('analytics')}
|
|
||||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
|
||||||
<Activity size={18} className="text-stone-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-stone-800 font-medium text-sm">Reset Analytics</p>
|
|
||||||
<p className="text-stone-500 text-xs">Clear analytics data</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('emails')}
|
|
||||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
|
||||||
<Mail size={18} className="text-stone-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-stone-800 font-medium text-sm">View Messages</p>
|
|
||||||
<p className="text-stone-500 text-xs">{stats.unreadEmails} unread messages</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('analytics')}
|
|
||||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
|
||||||
<TrendingUp size={18} className="text-stone-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-stone-800 font-medium text-sm">Analytics</p>
|
|
||||||
<p className="text-stone-500 text-xs">View detailed statistics</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('settings')}
|
|
||||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
|
||||||
<Settings size={18} className="text-stone-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-stone-800 font-medium text-sm">Settings</p>
|
|
||||||
<p className="text-stone-500 text-xs">System configuration</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeTab === 'projects' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold text-stone-900">Project Management</h2>
|
|
||||||
<p className="text-stone-500 mt-1">Manage your portfolio projects</p>
|
|
||||||
</div>
|
|
||||||
</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-2xl font-bold text-stone-900">System Settings</h1>
|
|
||||||
<p className="text-stone-500">Manage system configuration and preferences</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<div className="admin-glass-card p-6 rounded-xl">
|
|
||||||
<h2 className="text-xl font-bold text-stone-900 mb-4">Import / Export</h2>
|
|
||||||
<p className="text-stone-500 mb-4">Backup and restore your portfolio data</p>
|
|
||||||
<ImportExport />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-glass-card p-6 rounded-xl">
|
|
||||||
<h2 className="text-xl font-bold text-stone-900 mb-4">System Status</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between p-3 bg-stone-50 rounded-lg border border-stone-100">
|
|
||||||
<span className="text-stone-600">Database</span>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="w-4 h-4 bg-green-500 rounded-full animate-pulse"></div>
|
|
||||||
<span className="text-green-600 font-medium">Online</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between p-3 bg-stone-50 rounded-lg border border-stone-100">
|
|
||||||
<span className="text-stone-600">Redis Cache</span>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="w-4 h-4 bg-green-500 rounded-full animate-pulse"></div>
|
|
||||||
<span className="text-green-600 font-medium">Online</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between p-3 bg-stone-50 rounded-lg border border-stone-100">
|
|
||||||
<span className="text-stone-600">API Services</span>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="w-4 h-4 bg-green-500 rounded-full animate-pulse"></div>
|
|
||||||
<span className="text-green-600 font-medium">Online</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModernAdminDashboard;
|
export default ModernAdminDashboard;
|
||||||
|
|||||||
Reference in New Issue
Block a user