- Fixed authentication system (removed HTTP Basic Auth popup) - Added session-based authentication with proper logout - Updated rate limiting (20 req/s for login, 5 req/m for admin) - Created production deployment scripts and configs - Updated nginx configuration for dk0.dev domain - Added comprehensive production deployment guide - Fixed logout button functionality - Optimized for production with proper resource limits
577 lines
22 KiB
TypeScript
577 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import {
|
|
BarChart3,
|
|
TrendingUp,
|
|
Eye,
|
|
Heart,
|
|
Zap,
|
|
Globe,
|
|
Activity,
|
|
Target,
|
|
Award,
|
|
RefreshCw,
|
|
MousePointer,
|
|
RotateCcw,
|
|
Trash2,
|
|
AlertTriangle
|
|
} from 'lucide-react';
|
|
|
|
interface AnalyticsData {
|
|
overview: {
|
|
totalProjects: number;
|
|
publishedProjects: number;
|
|
featuredProjects: number;
|
|
totalViews: number;
|
|
totalLikes: number;
|
|
totalShares: number;
|
|
avgLighthouse: number;
|
|
};
|
|
projects: Array<{
|
|
id: number;
|
|
title: string;
|
|
category: string;
|
|
difficulty: string;
|
|
views: number;
|
|
likes: number;
|
|
shares: number;
|
|
lighthouse: number;
|
|
published: boolean;
|
|
featured: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}>;
|
|
categories: Record<string, number>;
|
|
difficulties: Record<string, number>;
|
|
performance: {
|
|
avgLighthouse: number;
|
|
totalViews: number;
|
|
totalLikes: number;
|
|
totalShares: number;
|
|
};
|
|
metrics: {
|
|
bounceRate: number;
|
|
avgSessionDuration: number;
|
|
pagesPerSession: number;
|
|
newUsers: number;
|
|
};
|
|
}
|
|
|
|
interface AnalyticsDashboardProps {
|
|
isAuthenticated: boolean;
|
|
}
|
|
|
|
export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps) {
|
|
const [data, setData] = useState<AnalyticsData | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [timeRange, setTimeRange] = useState<'7d' | '30d' | '90d' | '1y'>('30d');
|
|
const [showResetModal, setShowResetModal] = useState(false);
|
|
const [resetType, setResetType] = useState<'analytics' | 'pageviews' | 'interactions' | 'performance' | 'all'>('analytics');
|
|
const [resetting, setResetting] = useState(false);
|
|
|
|
const fetchAnalyticsData = useCallback(async () => {
|
|
if (!isAuthenticated) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const [analyticsRes, performanceRes] = await Promise.all([
|
|
fetch('/api/analytics/dashboard', {
|
|
headers: { 'x-admin-request': 'true' }
|
|
}),
|
|
fetch('/api/analytics/performance', {
|
|
headers: { 'x-admin-request': 'true' }
|
|
})
|
|
]);
|
|
|
|
if (!analyticsRes.ok || !performanceRes.ok) {
|
|
throw new Error('Failed to fetch analytics data');
|
|
}
|
|
|
|
const [analytics, performance] = await Promise.all([
|
|
analyticsRes.json(),
|
|
performanceRes.json()
|
|
]);
|
|
|
|
setData({
|
|
overview: analytics.overview || {
|
|
totalProjects: 0,
|
|
publishedProjects: 0,
|
|
featuredProjects: 0,
|
|
totalViews: 0,
|
|
totalLikes: 0,
|
|
totalShares: 0,
|
|
avgLighthouse: 90
|
|
},
|
|
projects: analytics.projects || [],
|
|
categories: analytics.categories || {},
|
|
difficulties: analytics.difficulties || {},
|
|
performance: performance.performance || {
|
|
avgLighthouse: 90,
|
|
totalViews: 0,
|
|
totalLikes: 0,
|
|
totalShares: 0
|
|
},
|
|
metrics: performance.metrics || {
|
|
bounceRate: 35,
|
|
avgSessionDuration: 180,
|
|
pagesPerSession: 2.5,
|
|
newUsers: 0
|
|
}
|
|
});
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load analytics');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
const resetAnalytics = async () => {
|
|
if (!isAuthenticated || resetting) return;
|
|
|
|
setResetting(true);
|
|
try {
|
|
const response = await fetch('/api/analytics/reset', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-admin-request': 'true'
|
|
},
|
|
body: JSON.stringify({ type: resetType })
|
|
});
|
|
|
|
if (response.ok) {
|
|
await fetchAnalyticsData(); // Refresh data
|
|
setShowResetModal(false);
|
|
} else {
|
|
const errorData = await response.json();
|
|
setError(errorData.error || 'Failed to reset analytics');
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to reset analytics');
|
|
console.error('Reset error:', err);
|
|
} finally {
|
|
setResetting(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
fetchAnalyticsData();
|
|
}
|
|
}, [isAuthenticated, fetchAnalyticsData]);
|
|
|
|
const StatCard = ({ title, value, icon: Icon, color, trend, trendValue, description }: {
|
|
title: string;
|
|
value: number | string;
|
|
icon: React.ComponentType<{ className?: string; size?: number }>;
|
|
color: string;
|
|
trend?: 'up' | 'down' | 'neutral';
|
|
trendValue?: string;
|
|
description?: string;
|
|
}) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="admin-glass-card p-6 rounded-xl hover:scale-105 transition-all duration-200"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center space-x-3 mb-4">
|
|
<div className={`p-3 rounded-xl ${color}`}>
|
|
<Icon className="w-6 h-6 text-white" size={24} />
|
|
</div>
|
|
<div>
|
|
<p className="text-white/60 text-sm font-medium">{title}</p>
|
|
{description && <p className="text-white/40 text-xs">{description}</p>}
|
|
</div>
|
|
</div>
|
|
<p className="text-3xl font-bold text-white mb-2">{value}</p>
|
|
{trend && trendValue && (
|
|
<div className={`flex items-center space-x-1 text-sm ${
|
|
trend === 'up' ? 'text-green-400' :
|
|
trend === 'down' ? 'text-red-400' : 'text-yellow-400'
|
|
}`}>
|
|
<TrendingUp className={`w-4 h-4 ${trend === 'down' ? 'rotate-180' : ''}`} />
|
|
<span>{trendValue}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
|
|
const getDifficultyColor = (difficulty: string) => {
|
|
switch (difficulty) {
|
|
case 'Beginner': return 'bg-green-500/30 text-green-400 border-green-500/40';
|
|
case 'Intermediate': return 'bg-yellow-500/30 text-yellow-400 border-yellow-500/40';
|
|
case 'Advanced': return 'bg-orange-500/30 text-orange-400 border-orange-500/40';
|
|
case 'Expert': return 'bg-red-500/30 text-red-400 border-red-500/40';
|
|
default: return 'bg-gray-500/30 text-gray-400 border-gray-500/40';
|
|
}
|
|
};
|
|
|
|
const getCategoryColor = (index: number) => {
|
|
const colors = [
|
|
'bg-blue-500/30 text-blue-400',
|
|
'bg-purple-500/30 text-purple-400',
|
|
'bg-green-500/30 text-green-400',
|
|
'bg-pink-500/30 text-pink-400',
|
|
'bg-indigo-500/30 text-indigo-400'
|
|
];
|
|
return colors[index % colors.length];
|
|
};
|
|
|
|
// Authentication disabled - show analytics directly
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-white flex items-center">
|
|
<BarChart3 className="w-8 h-8 mr-3 text-blue-400" />
|
|
Analytics Dashboard
|
|
</h1>
|
|
<p className="text-white/80 mt-2">Portfolio performance and user engagement metrics</p>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
{/* Time Range Selector */}
|
|
<div className="flex items-center space-x-1 admin-glass-light rounded-xl p-1">
|
|
{(['7d', '30d', '90d', '1y'] as const).map((range) => (
|
|
<button
|
|
key={range}
|
|
onClick={() => setTimeRange(range)}
|
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
|
timeRange === range
|
|
? 'bg-blue-500/40 text-blue-300 shadow-lg'
|
|
: 'text-white/70 hover:text-white hover:bg-white/10'
|
|
}`}
|
|
>
|
|
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : range === '90d' ? '90 Days' : '1 Year'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={fetchAnalyticsData}
|
|
disabled={loading}
|
|
className="flex items-center space-x-2 px-4 py-2 admin-glass-light rounded-xl hover:scale-105 transition-all duration-200 disabled:opacity-50"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 text-blue-400 ${loading ? 'animate-spin' : ''}`} />
|
|
<span className="text-white font-medium">Refresh</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setShowResetModal(true)}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-red-600/20 text-red-400 border border-red-500/30 rounded-xl hover:bg-red-600/30 hover:scale-105 transition-all"
|
|
>
|
|
<RotateCcw className="w-4 h-4" />
|
|
<span>Reset</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{loading && (
|
|
<div className="admin-glass-card p-8 rounded-xl">
|
|
<div className="flex items-center justify-center space-x-3">
|
|
<RefreshCw className="w-6 h-6 text-blue-400 animate-spin" />
|
|
<span className="text-white/80 text-lg">Loading analytics data...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="admin-glass-card p-6 rounded-xl border border-red-500/40">
|
|
<div className="flex items-center space-x-3 text-red-300">
|
|
<Activity className="w-5 h-5" />
|
|
<span>Error: {error}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{data && !loading && (
|
|
<>
|
|
{/* Overview Stats */}
|
|
<div>
|
|
<h2 className="text-xl font-bold text-white mb-6 flex items-center">
|
|
<Target className="w-5 h-5 mr-2 text-purple-400" />
|
|
Overview
|
|
</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
<StatCard
|
|
title="Total Views"
|
|
value={data.overview.totalViews.toLocaleString()}
|
|
icon={Eye}
|
|
color="bg-blue-500/30"
|
|
trend="up"
|
|
trendValue="+12.5%"
|
|
description="All-time page views"
|
|
/>
|
|
<StatCard
|
|
title="Projects"
|
|
value={data.overview.totalProjects}
|
|
icon={Globe}
|
|
color="bg-green-500/30"
|
|
trend="up"
|
|
trendValue="+2"
|
|
description={`${data.overview.publishedProjects} published`}
|
|
/>
|
|
<StatCard
|
|
title="Engagement"
|
|
value={data.overview.totalLikes}
|
|
icon={Heart}
|
|
color="bg-pink-500/30"
|
|
trend="up"
|
|
trendValue="+8.2%"
|
|
description="Total likes & shares"
|
|
/>
|
|
<StatCard
|
|
title="Performance"
|
|
value={data.overview.avgLighthouse}
|
|
icon={Zap}
|
|
color="bg-orange-500/30"
|
|
trend="up"
|
|
trendValue="+5%"
|
|
description="Avg Lighthouse score"
|
|
/>
|
|
<StatCard
|
|
title="Bounce Rate"
|
|
value={`${data.metrics.bounceRate}%`}
|
|
icon={MousePointer}
|
|
color="bg-purple-500/30"
|
|
trend="down"
|
|
trendValue="-2.1%"
|
|
description="User retention"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Project Performance */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Top Projects */}
|
|
<div className="admin-glass-card p-6 rounded-xl">
|
|
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
|
<Award className="w-5 h-5 mr-2 text-yellow-400" />
|
|
Top Performing Projects
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{data.projects
|
|
.sort((a, b) => b.views - a.views)
|
|
.slice(0, 5)
|
|
.map((project, index) => (
|
|
<motion.div
|
|
key={project.id}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="flex items-center justify-between p-4 admin-glass-light rounded-xl"
|
|
>
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center text-white font-bold">
|
|
#{index + 1}
|
|
</div>
|
|
<div>
|
|
<p className="text-white font-medium">{project.title}</p>
|
|
<p className="text-white/60 text-sm">{project.category}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-white font-bold">{project.views.toLocaleString()}</p>
|
|
<p className="text-white/60 text-sm">views</p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Categories Distribution */}
|
|
<div className="admin-glass-card p-6 rounded-xl">
|
|
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
|
<BarChart3 className="w-5 h-5 mr-2 text-green-400" />
|
|
Categories
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{Object.entries(data.categories).map(([category, count], index) => (
|
|
<motion.div
|
|
key={category}
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="flex items-center justify-between"
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
<div className={`w-4 h-4 rounded-full ${getCategoryColor(index)}`}></div>
|
|
<span className="text-white font-medium">{category}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-32 h-2 bg-white/10 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${getCategoryColor(index)} transition-all duration-500`}
|
|
style={{ width: `${(count / Math.max(...Object.values(data.categories))) * 100}%` }}
|
|
></div>
|
|
</div>
|
|
<span className="text-white/80 font-medium w-8 text-right">{count}</span>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Difficulty & Engagement */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Difficulty Distribution */}
|
|
<div className="admin-glass-card p-6 rounded-xl">
|
|
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
|
<Target className="w-5 h-5 mr-2 text-red-400" />
|
|
Difficulty Levels
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{Object.entries(data.difficulties).map(([difficulty, count]) => (
|
|
<motion.div
|
|
key={difficulty}
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className={`p-4 rounded-xl border ${getDifficultyColor(difficulty)}`}
|
|
>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold mb-1">{count}</p>
|
|
<p className="text-sm font-medium">{difficulty}</p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Activity */}
|
|
<div className="admin-glass-card p-6 rounded-xl">
|
|
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
|
<Activity className="w-5 h-5 mr-2 text-blue-400" />
|
|
Recent Activity
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{data.projects
|
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
.slice(0, 4)
|
|
.map((project, index) => (
|
|
<motion.div
|
|
key={project.id}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="flex items-center space-x-4 p-3 admin-glass-light rounded-xl"
|
|
>
|
|
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
|
<div className="flex-1">
|
|
<p className="text-white font-medium text-sm">{project.title}</p>
|
|
<p className="text-white/60 text-xs">
|
|
Updated {new Date(project.updatedAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{project.featured && (
|
|
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 rounded-full text-xs">
|
|
Featured
|
|
</span>
|
|
)}
|
|
<span className={`px-2 py-1 rounded-full text-xs ${
|
|
project.published
|
|
? 'bg-green-500/20 text-green-400'
|
|
: 'bg-yellow-500/20 text-yellow-400'
|
|
}`}>
|
|
{project.published ? 'Live' : 'Draft'}
|
|
</span>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Reset Modal */}
|
|
{showResetModal && (
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.95 }}
|
|
className="admin-glass-card rounded-2xl p-6 w-full max-w-md"
|
|
>
|
|
<div className="flex items-center space-x-3 mb-4">
|
|
<div className="w-10 h-10 bg-red-500/20 rounded-lg flex items-center justify-center">
|
|
<AlertTriangle className="w-5 h-5 text-red-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-white">Reset Analytics Data</h3>
|
|
<p className="text-white/60 text-sm">This action cannot be undone</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4 mb-6">
|
|
<div>
|
|
<label className="block text-white/80 text-sm mb-2">Reset Type</label>
|
|
<select
|
|
value={resetType}
|
|
onChange={(e) => setResetType(e.target.value as 'all' | 'performance' | 'analytics')}
|
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-red-500"
|
|
>
|
|
<option value="analytics">Analytics Only (views, likes, shares)</option>
|
|
<option value="pageviews">Page Views Only</option>
|
|
<option value="interactions">User Interactions Only</option>
|
|
<option value="performance">Performance Metrics Only</option>
|
|
<option value="all">Everything (Complete Reset)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
|
|
<div className="flex items-start space-x-2">
|
|
<AlertTriangle className="w-4 h-4 text-red-400 mt-0.5 flex-shrink-0" />
|
|
<div className="text-sm text-red-300">
|
|
<p className="font-medium mb-1">Warning:</p>
|
|
<p>This will permanently delete the selected analytics data. This action cannot be reversed.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
onClick={() => setShowResetModal(false)}
|
|
disabled={resetting}
|
|
className="flex-1 px-4 py-2 admin-glass-light text-white rounded-lg hover:scale-105 transition-all disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={resetAnalytics}
|
|
disabled={resetting}
|
|
className="flex-1 flex items-center justify-center space-x-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg hover:scale-105 transition-all disabled:opacity-50"
|
|
>
|
|
{resetting ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
<span>Resetting...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Trash2 className="w-4 h-4" />
|
|
<span>Reset {resetType === 'all' ? 'Everything' : 'Data'}</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AnalyticsDashboard;
|